From a203f9c8d837fb569ac3ccf7d1cd3ea70142a10e Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Mon, 11 May 2020 13:14:10 -0400 Subject: [PATCH 001/205] Reformat download status page template --- src/allmydata/web/download-status.xhtml | 89 +++++++++++++------------ 1 file changed, 47 insertions(+), 42 deletions(-) diff --git a/src/allmydata/web/download-status.xhtml b/src/allmydata/web/download-status.xhtml index e5794ca79..e47f1f1f8 100644 --- a/src/allmydata/web/download-status.xhtml +++ b/src/allmydata/web/download-status.xhtml @@ -1,58 +1,63 @@ + Tahoe-LAFS - File Download Status + -

File Download Status

+

File Download Status

- - -
- -
-

Download Results

- -
-
Return to the Welcome Page
+
+ +
+ +

Download Results

+ + +
+ +
Return to the Welcome Page
+ From 8a632b666826651a749699883ece477c9e2e025e Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Mon, 11 May 2020 13:20:52 -0400 Subject: [PATCH 002/205] Use twisted tags in download status page template --- src/allmydata/web/download-status.xhtml | 56 ++++++++++++------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/src/allmydata/web/download-status.xhtml b/src/allmydata/web/download-status.xhtml index e47f1f1f8..93bd08408 100644 --- a/src/allmydata/web/download-status.xhtml +++ b/src/allmydata/web/download-status.xhtml @@ -1,4 +1,4 @@ - + Tahoe-LAFS - File Download Status @@ -12,46 +12,46 @@

File Download Status

-
+
-
+

Download Results

    -
  • -
  • Servermap:
  • -
  • +
  • +
  • Servermap:
  • +
  • Timings:
    • -
    • File Size: bytes
    • -
    • Total: - ()
    • +
    • File Size: bytes
    • +
    • Total: + ()
      • -
      • Peer Selection:
      • -
      • UEB Fetch:
      • -
      • Hashtree Fetch:
      • -
      • Segment Fetch: - ()
      • +
      • Peer Selection:
      • +
      • UEB Fetch:
      • +
      • Hashtree Fetch:
      • +
      • Segment Fetch: + ()
        • -
        • Cumulative Fetching: - ()
        • -
        • Cumulative Decoding: - ()
        • -
        • Cumulative Decrypting: - ()
        • +
        • Cumulative Fetching: + ()
        • +
        • Cumulative Decoding: + ()
        • +
        • Cumulative Decrypting: + ()
        -
      • Paused by client:
      • +
      • Paused by client:
      -
    • +
From c14c4371523d371501e88f0d0103235729f7613e Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Mon, 11 May 2020 13:34:18 -0400 Subject: [PATCH 003/205] Make DownloadStatusPage a MultiFormatResource --- src/allmydata/web/status.py | 65 ++++++++++++++++++++++++------------- 1 file changed, 42 insertions(+), 23 deletions(-) diff --git a/src/allmydata/web/status.py b/src/allmydata/web/status.py index b20aa679e..85417fcee 100644 --- a/src/allmydata/web/status.py +++ b/src/allmydata/web/status.py @@ -489,13 +489,25 @@ class _EventJson(Resource, object): return json.dumps(data, indent=1) + "\n" -class DownloadStatusPage(DownloadResultsRendererMixin, rend.Page): - docFactory = getxmlfile("download-status.xhtml") +class DownloadStatusPage(MultiFormatResource): - def __init__(self, data): - rend.Page.__init__(self, data) - self.download_status = data - self.putChild("event_json", _EventJson(self.download_status)) + def __init__(self, download_status): + super(DownloadStatusPage, self).__init__() + self._download_status = download_status + self.putChild("event_json", _EventJson(self._download_status)) + + def render_HTML(self, req): + elem = DownloadStatusElement(self._download_status) + return renderElement(req, elem) + + +class DownloadStatusElement(Element, DownloadResultsRendererMixin): + + loader = XMLFile(FilePath(__file__).sibling("download-status.xhtml")) + + def __init__(self, download_status): + super(DownloadStatusElement, self).__init__() + self._download_status = download_status def download_results(self): return defer.maybeDeferred(self.download_status.get_results) @@ -645,33 +657,40 @@ class DownloadStatusPage(DownloadResultsRendererMixin, rend.Page): d.addCallback(_got_results) return d - def render_started(self, ctx, data): - started_s = render_time(data.get_started()) - return started_s + " (%s)" % data.get_started() + @renderer + def started(self, req, tag): + started_s = render_time(self._download_status.get_started()) + return tag(started_s + " (%s)" % self._download_status.get_started()) - def render_si(self, ctx, data): - si_s = base32.b2a_or_none(data.get_storage_index()) + @renderer + def si(self, req, tag): + si_s = base32.b2a_or_none(self._download_status.get_storage_index()) if si_s is None: si_s = "(None)" - return si_s + return tag(si_s) - def render_helper(self, ctx, data): - return {True: "Yes", - False: "No"}[data.using_helper()] + @renderer + def helper(self, req, tag): + return tag({True: "Yes", + False: "No"}[self._download_status.using_helper()]) - def render_total_size(self, ctx, data): - size = data.get_size() + @renderer + def total_size(self, req, tag): + size = self._download_status.get_size() if size is None: return "(unknown)" - return size + return tag(str(size)) - def render_progress(self, ctx, data): - progress = data.get_progress() + @renderer + def progress(self, req, tag): + progress = self._download_status.get_progress() # TODO: make an ascii-art bar - return "%.1f%%" % (100.0 * progress) + return tag("%.1f%%" % (100.0 * progress)) + + @renderer + def status(self, req, tag): + return tag(self._download_status.get_status()) - def render_status(self, ctx, data): - return data.get_status() From c4b63ada55d7f1a50ffb2941c677ed2f43213eff Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Mon, 11 May 2020 14:24:26 -0400 Subject: [PATCH 004/205] Update events tables renderer --- src/allmydata/web/status.py | 172 +++++++++++++++++++++++------------- 1 file changed, 109 insertions(+), 63 deletions(-) diff --git a/src/allmydata/web/status.py b/src/allmydata/web/status.py index 85417fcee..bdc5874f5 100644 --- a/src/allmydata/web/status.py +++ b/src/allmydata/web/status.py @@ -512,14 +512,15 @@ class DownloadStatusElement(Element, DownloadResultsRendererMixin): def download_results(self): return defer.maybeDeferred(self.download_status.get_results) - def relative_time(self, t): + def _relative_time(self, t): if t is None: return t - if self.download_status.first_timestamp is not None: - return t - self.download_status.first_timestamp + if self._download_status.first_timestamp is not None: + return t - self._download_status.first_timestamp return t - def short_relative_time(self, t): - t = self.relative_time(t) + + def _short_relative_time(self, t): + t = self._relative_time(t) if t is None: return "" return "+%.6fs" % t @@ -532,19 +533,27 @@ class DownloadStatusElement(Element, DownloadResultsRendererMixin): time_s = self.render_time(None, seconds) if seconds != 0: rate = self.render_rate(None, 1.0 * bytes / seconds) - return T.span(title=rate)[time_s] - return T.span[time_s] + return tags.span(time_s, title=rate) + return tags.span(time_s) - def render_events(self, ctx, data): - if not self.download_status.storage_index: + @renderer + def events(self, req, tag): + if not self._download_status.storage_index: return - srt = self.short_relative_time - l = T.div() - t = T.table(align="left", class_="status-download-events") - t[T.tr[T.th["serverid"], T.th["sent"], T.th["received"], - T.th["shnums"], T.th["RTT"]]] - for d_ev in self.download_status.dyhb_requests: + srt = self._short_relative_time + + l = tags.div() + + t = tags.table(align="left", class_="status-download-events") + + t(tags.tr(tags.th("serverid"), + tags.th("sent"), + tags.th("received"), + tags.th("shnums"), + tags.th("RTT"))) + + for d_ev in self._download_status.dyhb_requests: server = d_ev["server"] sent = d_ev["start_time"] shnums = d_ev["response_shnums"] @@ -554,20 +563,31 @@ class DownloadStatusElement(Element, DownloadResultsRendererMixin): rtt = received - sent if not shnums: shnums = ["-"] - t[T.tr(style="background: %s" % _color(server))[ - [T.td[server.get_name()], T.td[srt(sent)], T.td[srt(received)], - T.td[",".join([str(shnum) for shnum in shnums])], - T.td[self.render_time(None, rtt)], - ]]] - l[T.h2["DYHB Requests:"], t] - l[T.br(clear="all")] + t(tags.tr(style="background: %s" % _color(server))( + (tags.td(server.get_name()), + tags.td(srt(sent)), + tags.td(srt(received)), + tags.td(",".join([str(shnum) for shnum in shnums])), + tags.td(self.render_time(None, rtt)), + ))) - t = T.table(align="left",class_="status-download-events") - t[T.tr[T.th["range"], T.th["start"], T.th["finish"], T.th["got"], - T.th["time"], T.th["decrypttime"], T.th["pausedtime"], - T.th["speed"]]] - for r_ev in self.download_status.read_events: + l(tags.h2("DYHB Requests:"), t) + l(tags.br(clear="all")) + + t = tags.table(align="left",class_="status-download-events") + + t(tags.tr(( + tags.th("range"), + tags.th("start"), + tags.th("finish"), + tags.th("got"), + tags.th("time"), + tags.th("decrypttime"), + tags.th("pausedtime"), + tags.th("speed")))) + + for r_ev in self._download_status.read_events: start = r_ev["start"] length = r_ev["length"] bytes = r_ev["bytes_returned"] @@ -581,21 +601,33 @@ class DownloadStatusElement(Element, DownloadResultsRendererMixin): rtt = self.render_time(None, rtt) paused = self.render_time(None, r_ev["paused_time"]) - t[T.tr[T.td["[%d:+%d]" % (start, length)], - T.td[srt(r_ev["start_time"])], T.td[srt(r_ev["finish_time"])], - T.td[bytes], T.td[rtt], - T.td[decrypt_time], T.td[paused], - T.td[speed], - ]] + t(tags.tr( + tags.td("[%d:+%d]" % (start, length)), + tags.td(srt(r_ev["start_time"])), + tags.td(srt(r_ev["finish_time"])), + tags.td(str(bytes)), + tags.td(rtt), + tags.td(decrypt_time), + tags.td(paused), + tags.td(speed), + )) - l[T.h2["Read Events:"], t] - l[T.br(clear="all")] + l(tags.h2("Read Events:"), t) + l(tags.br(clear="all")) - t = T.table(align="left",class_="status-download-events") - t[T.tr[T.th["segnum"], T.th["start"], T.th["active"], T.th["finish"], - T.th["range"], - T.th["decodetime"], T.th["segtime"], T.th["speed"]]] - for s_ev in self.download_status.segment_events: + t = tags.table(align="left",class_="status-download-events") + + t(tags.tr( + tags.th("segnum"), + tags.th("start"), + tags.th("active"), + tags.th("finish"), + tags.th("range"), + tags.th("decodetime"), + tags.th("segtime"), + tags.th("speed"))) + + for s_ev in self._download_status.segment_events: range_s = "-" segtime_s = "-" speed = "-" @@ -615,36 +647,50 @@ class DownloadStatusElement(Element, DownloadResultsRendererMixin): # not finished yet pass - t[T.tr[T.td["seg%d" % s_ev["segment_number"]], - T.td[srt(s_ev["start_time"])], - T.td[srt(s_ev["active_time"])], - T.td[srt(s_ev["finish_time"])], - T.td[range_s], - T.td[decode_time], - T.td[segtime_s], T.td[speed]]] + t(tags.tr( + tags.td("seg%d" % s_ev["segment_number"]), + tags.td(srt(s_ev["start_time"])), + tags.td(srt(s_ev["active_time"])), + tags.td(srt(s_ev["finish_time"])), + tags.td(range_s), + tags.td(decode_time), + tags.td(segtime_s), + tags.td(speed))) - l[T.h2["Segment Events:"], t] - l[T.br(clear="all")] - t = T.table(align="left",class_="status-download-events") - t[T.tr[T.th["serverid"], T.th["shnum"], T.th["range"], - T.th["txtime"], T.th["rxtime"], - T.th["received"], T.th["RTT"]]] - for r_ev in self.download_status.block_requests: + l(tags.h2("Segment Events:"), t) + + l(tags.br(clear="all")) + + t = tags.table(align="left",class_="status-download-events") + + t(tags.tr( + tags.th("serverid"), + tags.th("shnum"), + tags.th("range"), + tags.th("txtime"), + tags.th("rxtime"), + tags.th("received"), + tags.th("RTT"))) + + for r_ev in self._download_status.block_requests: server = r_ev["server"] rtt = None if r_ev["finish_time"] is not None: rtt = r_ev["finish_time"] - r_ev["start_time"] color = _color(server) - t[T.tr(style="background: %s" % color)[ - T.td[server.get_name()], T.td[r_ev["shnum"]], - T.td["[%d:+%d]" % (r_ev["start"], r_ev["length"])], - T.td[srt(r_ev["start_time"])], T.td[srt(r_ev["finish_time"])], - T.td[r_ev["response_length"] or ""], - T.td[self.render_time(None, rtt)], - ]] + t(tags.tr(style="background: %s" % color) + ( + tags.td(server.get_name()), + tags.td(str(r_ev["shnum"])), + tags.td("[%d:+%d]" % (r_ev["start"], r_ev["length"])), + tags.td(srt(r_ev["start_time"])), + tags.td(srt(r_ev["finish_time"])), + tags.td(str(r_ev["response_length"]) or ""), + tags.td(self.render_time(None, rtt)), + )) - l[T.h2["Requests:"], t] - l[T.br(clear="all")] + l(tags.h2("Requests:"), t) + l(tags.br(clear="all")) return l From c5b2073bf04aad921f48fd49bfde78d58c4b090b Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Mon, 11 May 2020 15:08:44 -0400 Subject: [PATCH 005/205] Avoid use of RateAndTimeMixin methods in DownloadStatusElement --- src/allmydata/web/status.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/allmydata/web/status.py b/src/allmydata/web/status.py index bdc5874f5..2afe18fef 100644 --- a/src/allmydata/web/status.py +++ b/src/allmydata/web/status.py @@ -510,7 +510,7 @@ class DownloadStatusElement(Element, DownloadResultsRendererMixin): self._download_status = download_status def download_results(self): - return defer.maybeDeferred(self.download_status.get_results) + return defer.maybeDeferred(self._download_status.get_results) def _relative_time(self, t): if t is None: @@ -530,9 +530,9 @@ class DownloadStatusElement(Element, DownloadResultsRendererMixin): return T.a(href=url.URL.fromContext(ctx).child("timeline"))["timeline"] def _rate_and_time(self, bytes, seconds): - time_s = self.render_time(None, seconds) + time_s = abbreviate_time(seconds) if seconds != 0: - rate = self.render_rate(None, 1.0 * bytes / seconds) + rate = abbreviate_rate(1.0 * bytes / seconds) return tags.span(time_s, title=rate) return tags.span(time_s) @@ -569,7 +569,7 @@ class DownloadStatusElement(Element, DownloadResultsRendererMixin): tags.td(srt(sent)), tags.td(srt(received)), tags.td(",".join([str(shnum) for shnum in shnums])), - tags.td(self.render_time(None, rtt)), + tags.td(abbreviate_time(rtt)), ))) l(tags.h2("DYHB Requests:"), t) @@ -597,9 +597,9 @@ class DownloadStatusElement(Element, DownloadResultsRendererMixin): speed, rtt = "","" if r_ev["finish_time"] is not None: rtt = r_ev["finish_time"] - r_ev["start_time"] - r_ev["paused_time"] - speed = self.render_rate(None, compute_rate(bytes, rtt)) - rtt = self.render_time(None, rtt) - paused = self.render_time(None, r_ev["paused_time"]) + speed = abbreviate_rate(compute_rate(bytes, rtt)) + rtt = abbreviate_time(rtt) + paused = abbreviate_time(r_ev["paused_time"]) t(tags.tr( tags.td("[%d:+%d]" % (start, length)), @@ -635,10 +635,10 @@ class DownloadStatusElement(Element, DownloadResultsRendererMixin): if s_ev["finish_time"] is not None: if s_ev["success"]: segtime = s_ev["finish_time"] - s_ev["active_time"] - segtime_s = self.render_time(None, segtime) + segtime_s = abbreviate_time(segtime) seglen = s_ev["segment_length"] range_s = "[%d:+%d]" % (s_ev["segment_start"], seglen) - speed = self.render_rate(None, compute_rate(seglen, segtime)) + speed = abbreviate_rate(compute_rate(seglen, segtime)) decode_time = self._rate_and_time(seglen, s_ev["decode_time"]) else: # error @@ -686,7 +686,7 @@ class DownloadStatusElement(Element, DownloadResultsRendererMixin): tags.td(srt(r_ev["start_time"])), tags.td(srt(r_ev["finish_time"])), tags.td(str(r_ev["response_length"]) or ""), - tags.td(self.render_time(None, rtt)), + tags.td(abbreviate_time(rtt)), )) l(tags.h2("Requests:"), t) From e0241655e5b24d5db4228b173a1950295dc1e574 Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Mon, 11 May 2020 15:09:16 -0400 Subject: [PATCH 006/205] Update results renderer --- src/allmydata/web/status.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/allmydata/web/status.py b/src/allmydata/web/status.py index 2afe18fef..1e6c05488 100644 --- a/src/allmydata/web/status.py +++ b/src/allmydata/web/status.py @@ -694,11 +694,12 @@ class DownloadStatusElement(Element, DownloadResultsRendererMixin): return l - def render_results(self, ctx, data): + @renderer + def results(self, req, tag): d = self.download_results() def _got_results(results): if results: - return ctx.tag + return tag return "" d.addCallback(_got_results) return d From 7e02502268a4083230e59389a041e1cd022ca2a0 Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Thu, 14 May 2020 11:41:54 -0400 Subject: [PATCH 007/205] Add comments to DownloadStatusPage --- src/allmydata/web/status.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/allmydata/web/status.py b/src/allmydata/web/status.py index 1e6c05488..9be4ee09a 100644 --- a/src/allmydata/web/status.py +++ b/src/allmydata/web/status.py @@ -490,8 +490,12 @@ class _EventJson(Resource, object): class DownloadStatusPage(MultiFormatResource): + """Renders /status/down-%d.""" def __init__(self, download_status): + """ + :param IDownloadStatus download_status: stats provider + """ super(DownloadStatusPage, self).__init__() self._download_status = download_status self.putChild("event_json", _EventJson(self._download_status)) From b02cfa5bbe8ff3a07156552464028f0db1a2b863 Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Thu, 14 May 2020 11:46:20 -0400 Subject: [PATCH 008/205] Use more descriptive variable names --- src/allmydata/web/status.py | 71 ++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 36 deletions(-) diff --git a/src/allmydata/web/status.py b/src/allmydata/web/status.py index 9be4ee09a..c4ad01dfe 100644 --- a/src/allmydata/web/status.py +++ b/src/allmydata/web/status.py @@ -547,15 +547,15 @@ class DownloadStatusElement(Element, DownloadResultsRendererMixin): srt = self._short_relative_time - l = tags.div() + evtag = tags.div() - t = tags.table(align="left", class_="status-download-events") + dyhbtag = tags.table(align="left", class_="status-download-events") - t(tags.tr(tags.th("serverid"), - tags.th("sent"), - tags.th("received"), - tags.th("shnums"), - tags.th("RTT"))) + dyhbtag(tags.tr(tags.th("serverid"), + tags.th("sent"), + tags.th("received"), + tags.th("shnums"), + tags.th("RTT"))) for d_ev in self._download_status.dyhb_requests: server = d_ev["server"] @@ -568,7 +568,7 @@ class DownloadStatusElement(Element, DownloadResultsRendererMixin): if not shnums: shnums = ["-"] - t(tags.tr(style="background: %s" % _color(server))( + dyhbtag(tags.tr(style="background: %s" % _color(server))( (tags.td(server.get_name()), tags.td(srt(sent)), tags.td(srt(received)), @@ -576,12 +576,12 @@ class DownloadStatusElement(Element, DownloadResultsRendererMixin): tags.td(abbreviate_time(rtt)), ))) - l(tags.h2("DYHB Requests:"), t) - l(tags.br(clear="all")) + evtag(tags.h2("DYHB Requests:"), dyhbtag) + evtag(tags.br(clear="all")) - t = tags.table(align="left",class_="status-download-events") + readtag = tags.table(align="left",class_="status-download-events") - t(tags.tr(( + readtag(tags.tr(( tags.th("range"), tags.th("start"), tags.th("finish"), @@ -605,7 +605,7 @@ class DownloadStatusElement(Element, DownloadResultsRendererMixin): rtt = abbreviate_time(rtt) paused = abbreviate_time(r_ev["paused_time"]) - t(tags.tr( + readtag(tags.tr( tags.td("[%d:+%d]" % (start, length)), tags.td(srt(r_ev["start_time"])), tags.td(srt(r_ev["finish_time"])), @@ -616,12 +616,12 @@ class DownloadStatusElement(Element, DownloadResultsRendererMixin): tags.td(speed), )) - l(tags.h2("Read Events:"), t) - l(tags.br(clear="all")) + evtag(tags.h2("Read Events:"), readtag) + evtag(tags.br(clear="all")) - t = tags.table(align="left",class_="status-download-events") + segtag = tags.table(align="left",class_="status-download-events") - t(tags.tr( + segtag(tags.tr( tags.th("segnum"), tags.th("start"), tags.th("active"), @@ -651,7 +651,7 @@ class DownloadStatusElement(Element, DownloadResultsRendererMixin): # not finished yet pass - t(tags.tr( + segtag(tags.tr( tags.td("seg%d" % s_ev["segment_number"]), tags.td(srt(s_ev["start_time"])), tags.td(srt(s_ev["active_time"])), @@ -661,13 +661,12 @@ class DownloadStatusElement(Element, DownloadResultsRendererMixin): tags.td(segtime_s), tags.td(speed))) - l(tags.h2("Segment Events:"), t) + evtag(tags.h2("Segment Events:"), segtag) + evtag(tags.br(clear="all")) - l(tags.br(clear="all")) + reqtab = tags.table(align="left",class_="status-download-events") - t = tags.table(align="left",class_="status-download-events") - - t(tags.tr( + reqtab(tags.tr( tags.th("serverid"), tags.th("shnum"), tags.th("range"), @@ -682,21 +681,21 @@ class DownloadStatusElement(Element, DownloadResultsRendererMixin): if r_ev["finish_time"] is not None: rtt = r_ev["finish_time"] - r_ev["start_time"] color = _color(server) - t(tags.tr(style="background: %s" % color) - ( - tags.td(server.get_name()), - tags.td(str(r_ev["shnum"])), - tags.td("[%d:+%d]" % (r_ev["start"], r_ev["length"])), - tags.td(srt(r_ev["start_time"])), - tags.td(srt(r_ev["finish_time"])), - tags.td(str(r_ev["response_length"]) or ""), - tags.td(abbreviate_time(rtt)), - )) + reqtab(tags.tr(style="background: %s" % color) + ( + tags.td(server.get_name()), + tags.td(str(r_ev["shnum"])), + tags.td("[%d:+%d]" % (r_ev["start"], r_ev["length"])), + tags.td(srt(r_ev["start_time"])), + tags.td(srt(r_ev["finish_time"])), + tags.td(str(r_ev["response_length"]) or ""), + tags.td(abbreviate_time(rtt)), + )) - l(tags.h2("Requests:"), t) - l(tags.br(clear="all")) + evtag(tags.h2("Requests:"), reqtab) + evtag(tags.br(clear="all")) - return l + return evtag @renderer def results(self, req, tag): From f3edc8a48cc2647e05fce7e063a06937b8f96c35 Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Thu, 14 May 2020 12:50:17 -0400 Subject: [PATCH 009/205] Add a note about rendering download events tables --- src/allmydata/web/status.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/allmydata/web/status.py b/src/allmydata/web/status.py index c4ad01dfe..209a01317 100644 --- a/src/allmydata/web/status.py +++ b/src/allmydata/web/status.py @@ -540,6 +540,9 @@ class DownloadStatusElement(Element, DownloadResultsRendererMixin): return tags.span(time_s, title=rate) return tags.span(time_s) + # XXX: This method is a candidate for refactoring. It renders + # four tables from this function. Layout part of those tables + # could be moved to download-status.xhtml. @renderer def events(self, req, tag): if not self._download_status.storage_index: From 1f1f3b5f6100b94122ce01f5bcb669a4b42f7b33 Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Thu, 14 May 2020 12:59:06 -0400 Subject: [PATCH 010/205] Remove unused render_timeline_link() As it turns out, nothing is using this method. --- src/allmydata/web/status.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/allmydata/web/status.py b/src/allmydata/web/status.py index 209a01317..b6d8b18e4 100644 --- a/src/allmydata/web/status.py +++ b/src/allmydata/web/status.py @@ -529,10 +529,6 @@ class DownloadStatusElement(Element, DownloadResultsRendererMixin): return "" return "+%.6fs" % t - def render_timeline_link(self, ctx, data): - from nevow import url - return T.a(href=url.URL.fromContext(ctx).child("timeline"))["timeline"] - def _rate_and_time(self, bytes, seconds): time_s = abbreviate_time(seconds) if seconds != 0: From 1fa77d89834ac64035f26a41c9e5561f49cbb89e Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Thu, 14 May 2020 14:07:19 -0400 Subject: [PATCH 011/205] Avoid use of DownloadResultsRendererMixin --- src/allmydata/web/status.py | 263 +++++++++++++++++++----------------- 1 file changed, 139 insertions(+), 124 deletions(-) diff --git a/src/allmydata/web/status.py b/src/allmydata/web/status.py index b6d8b18e4..0628773da 100644 --- a/src/allmydata/web/status.py +++ b/src/allmydata/web/status.py @@ -217,129 +217,6 @@ class UploadStatusPage(UploadResultsRendererMixin, rend.Page): def render_status(self, ctx, data): return data.get_status() -class DownloadResultsRendererMixin(RateAndTimeMixin): - # this requires a method named 'download_results' - - def render_servermap(self, ctx, data): - d = self.download_results() - d.addCallback(lambda res: res.servermap) - def _render(servermap): - if servermap is None: - return "None" - l = T.ul() - for peerid in sorted(servermap.keys()): - peerid_s = idlib.shortnodeid_b2a(peerid) - shares_s = ",".join(["#%d" % shnum - for shnum in servermap[peerid]]) - l[T.li["[%s] has share%s: %s" % (peerid_s, - plural(servermap[peerid]), - shares_s)]] - return l - d.addCallback(_render) - return d - - def render_servers_used(self, ctx, data): - d = self.download_results() - d.addCallback(lambda res: res.servers_used) - def _got(servers_used): - if not servers_used: - return "" - peerids_s = ", ".join(["[%s]" % idlib.shortnodeid_b2a(peerid) - for peerid in servers_used]) - return T.li["Servers Used: ", peerids_s] - d.addCallback(_got) - return d - - def render_problems(self, ctx, data): - d = self.download_results() - d.addCallback(lambda res: res.server_problems) - def _got(server_problems): - if not server_problems: - return "" - l = T.ul() - for peerid in sorted(server_problems.keys()): - peerid_s = idlib.shortnodeid_b2a(peerid) - l[T.li["[%s]: %s" % (peerid_s, server_problems[peerid])]] - return T.li["Server Problems:", l] - d.addCallback(_got) - return d - - def data_file_size(self, ctx, data): - d = self.download_results() - d.addCallback(lambda res: res.file_size) - return d - - def _get_time(self, name): - d = self.download_results() - d.addCallback(lambda res: res.timings.get(name)) - return d - - def data_time_total(self, ctx, data): - return self._get_time("total") - - def data_time_peer_selection(self, ctx, data): - return self._get_time("peer_selection") - - def data_time_uri_extension(self, ctx, data): - return self._get_time("uri_extension") - - def data_time_hashtrees(self, ctx, data): - return self._get_time("hashtrees") - - def data_time_segments(self, ctx, data): - return self._get_time("segments") - - def data_time_cumulative_fetch(self, ctx, data): - return self._get_time("cumulative_fetch") - - def data_time_cumulative_decode(self, ctx, data): - return self._get_time("cumulative_decode") - - def data_time_cumulative_decrypt(self, ctx, data): - return self._get_time("cumulative_decrypt") - - def data_time_paused(self, ctx, data): - return self._get_time("paused") - - def _get_rate(self, name): - d = self.download_results() - def _convert(r): - file_size = r.file_size - duration = r.timings.get(name) - return compute_rate(file_size, duration) - d.addCallback(_convert) - return d - - def data_rate_total(self, ctx, data): - return self._get_rate("total") - - def data_rate_segments(self, ctx, data): - return self._get_rate("segments") - - def data_rate_fetch(self, ctx, data): - return self._get_rate("cumulative_fetch") - - def data_rate_decode(self, ctx, data): - return self._get_rate("cumulative_decode") - - def data_rate_decrypt(self, ctx, data): - return self._get_rate("cumulative_decrypt") - - def render_server_timings(self, ctx, data): - d = self.download_results() - d.addCallback(lambda res: res.timings.get("fetch_per_server")) - def _render(per_server): - if per_server is None: - return "" - l = T.ul() - for peerid in sorted(per_server.keys()): - peerid_s = idlib.shortnodeid_b2a(peerid) - times_s = ", ".join([self.render_time(None, t) - for t in per_server[peerid]]) - l[T.li["[%s]: %s" % (peerid_s, times_s)]] - return T.li["Per-Server Segment Fetch Response Times: ", l] - d.addCallback(_render) - return d def _find_overlap(events, start_key, end_key): """ @@ -505,7 +382,7 @@ class DownloadStatusPage(MultiFormatResource): return renderElement(req, elem) -class DownloadStatusElement(Element, DownloadResultsRendererMixin): +class DownloadStatusElement(Element): loader = XMLFile(FilePath(__file__).sibling("download-status.xhtml")) @@ -740,7 +617,145 @@ class DownloadStatusElement(Element, DownloadResultsRendererMixin): def status(self, req, tag): return tag(self._download_status.get_status()) + @renderer + def servers_used(self, req, tag): + d = self.download_results() + d.addCallback(lambda res: res.servers_used) + def _got(servers_used): + if not servers_used: + return "" + peerids_s = ", ".join(["[%s]" % idlib.shortnodeid_b2a(peerid) + for peerid in servers_used]) + return tags.li("Servers Used: ", peerids_s) + d.addCallback(_got) + return d + @renderer + def servermap(self, req, tag): + d = self.download_results() + d.addCallback(lambda res: res.servermap) + def _render(servermap): + if servermap is None: + return tag("None") + ul = tags.ul() + for peerid in sorted(servermap.keys()): + peerid_s = idlib.shortnodeid_b2a(peerid) + shares_s = ",".join(["#%d" % shnum + for shnum in servermap[peerid]]) + ul(tags.li("[%s] has share%s: %s" % (peerid_s, + plural(servermap[peerid]), + shares_s))) + return ul + d.addCallback(_render) + return d + + @renderer + def problems(self, req, tag): + d = self.download_results() + d.addCallback(lambda res: res.server_problems) + def _got(server_problems): + if not server_problems: + return tag("") + ul = T.ul() + for peerid in sorted(server_problems.keys()): + peerid_s = idlib.shortnodeid_b2a(peerid) + ul(tags.li("[%s]: %s" % (peerid_s, server_problems[peerid]))) + return tags.li("Server Problems:", ul) + d.addCallback(_got) + return d + + @renderer + def file_size(self, req, tag): + d = self.download_results() + d.addCallback(lambda res: str(res.file_size)) + return d + + def _get_time(self, name): + d = self.download_results() + d.addCallback(lambda res: res.timings.get(name)) + return d + + @renderer + def time_total(self, req, tag): + return self._get_time("total") + + @renderer + def time_peer_selection(self, req, tag): + return self._get_time("peer_selection") + + @renderer + def time_uri_extension(self, req, tag): + return self._get_time("uri_extension") + + @renderer + def time_hashtrees(self, req, tag): + return self._get_time("hashtrees") + + @renderer + def time_segments(self, req, tag): + return self._get_time("segments") + + @renderer + def time_cumulative_fetch(self, req, tag): + return self._get_time("cumulative_fetch") + + @renderer + def time_cumulative_decode(self, req, tag): + return self._get_time("cumulative_decode") + + @renderer + def time_cumulative_decrypt(self, req, tag): + return self._get_time("cumulative_decrypt") + + @renderer + def time_paused(self, req, tag): + return self._get_time("paused") + + def _get_rate(self, name): + d = self.download_results() + def _convert(r): + file_size = r.file_size + duration = r.timings.get(name) + return compute_rate(file_size, duration) + d.addCallback(_convert) + return d + + @renderer + def rate_total(self, req, tag): + return self._get_rate("total") + + @renderer + def rate_segments(self, req, tag): + return self._get_rate("segments") + + @renderer + def rate_fetch(self, req, tag): + return self._get_rate("cumulative_fetch") + + @renderer + def rate_decode(self, req, tag): + return self._get_rate("cumulative_decode") + + @renderer + def rate_decrypt(self, req, tag): + return self._get_rate("cumulative_decrypt") + + @renderer + def server_timings(self, req, tag): + d = self.download_results() + d.addCallback(lambda res: res.timings.get("fetch_per_server")) + def _render(per_server): + if per_server is None: + return "" + ul = tags.ul() + for peerid in sorted(per_server.keys()): + peerid_s = idlib.shortnodeid_b2a(peerid) + times_s = ", ".join([self.render_time(None, t) + for t in per_server[peerid]]) + ul(tags.li("[%s]: %s" % (peerid_s, times_s))) + return tags.li("Per-Server Segment Fetch Response Times: ", ul) + d.addCallback(_render) + return d class RetrieveStatusPage(MultiFormatResource): From 68df408229431acaf0b2fdd67d76809368afd820 Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Thu, 14 May 2020 15:20:59 -0400 Subject: [PATCH 012/205] Add a note about unimplemented DownloadStatus method --- src/allmydata/web/status.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/allmydata/web/status.py b/src/allmydata/web/status.py index 0628773da..83e588bd9 100644 --- a/src/allmydata/web/status.py +++ b/src/allmydata/web/status.py @@ -390,6 +390,12 @@ class DownloadStatusElement(Element): super(DownloadStatusElement, self).__init__() self._download_status = download_status + # XXX: fun fact: the `get_results()` method which we wind up + # invoking here (see immutable.downloader.status.DownloadStatus) + # is unimplemented, and simply returns a `None`. As a result, + # `results()` renderer returns an empty tag, and does not invoke + # any of the subsequent renderers. Thus we end up not displaying + # download results on the download status page. def download_results(self): return defer.maybeDeferred(self._download_status.get_results) From 88e8854ad47c13249c346e4c5abfc6b7022a62ca Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Thu, 14 May 2020 15:28:00 -0400 Subject: [PATCH 013/205] Wrap renderer results in tags --- src/allmydata/web/status.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/allmydata/web/status.py b/src/allmydata/web/status.py index 83e588bd9..3d8ecb975 100644 --- a/src/allmydata/web/status.py +++ b/src/allmydata/web/status.py @@ -683,39 +683,39 @@ class DownloadStatusElement(Element): @renderer def time_total(self, req, tag): - return self._get_time("total") + return tag(str(self._get_time("total"))) @renderer def time_peer_selection(self, req, tag): - return self._get_time("peer_selection") + return tag(str(self._get_time("peer_selection"))) @renderer def time_uri_extension(self, req, tag): - return self._get_time("uri_extension") + return tag(str(self._get_time("uri_extension"))) @renderer def time_hashtrees(self, req, tag): - return self._get_time("hashtrees") + return tag(str(self._get_time("hashtrees"))) @renderer def time_segments(self, req, tag): - return self._get_time("segments") + return tag(str(self._get_time("segments"))) @renderer def time_cumulative_fetch(self, req, tag): - return self._get_time("cumulative_fetch") + return tag(str(self._get_time("cumulative_fetch"))) @renderer def time_cumulative_decode(self, req, tag): - return self._get_time("cumulative_decode") + return tag(str(self._get_time("cumulative_decode"))) @renderer def time_cumulative_decrypt(self, req, tag): - return self._get_time("cumulative_decrypt") + return tag(str(self._get_time("cumulative_decrypt"))) @renderer def time_paused(self, req, tag): - return self._get_time("paused") + return tag(str(self._get_time("paused"))) def _get_rate(self, name): d = self.download_results() @@ -728,23 +728,23 @@ class DownloadStatusElement(Element): @renderer def rate_total(self, req, tag): - return self._get_rate("total") + return tag(str(self._get_rate("total"))) @renderer def rate_segments(self, req, tag): - return self._get_rate("segments") + return tag(str(self._get_rate("segments"))) @renderer def rate_fetch(self, req, tag): - return self._get_rate("cumulative_fetch") + return tag(str(self._get_rate("cumulative_fetch"))) @renderer def rate_decode(self, req, tag): - return self._get_rate("cumulative_decode") + return tag(str(self._get_rate("cumulative_decode"))) @renderer def rate_decrypt(self, req, tag): - return self._get_rate("cumulative_decrypt") + return tag(str(self._get_rate("cumulative_decrypt"))) @renderer def server_timings(self, req, tag): @@ -756,7 +756,7 @@ class DownloadStatusElement(Element): ul = tags.ul() for peerid in sorted(per_server.keys()): peerid_s = idlib.shortnodeid_b2a(peerid) - times_s = ", ".join([self.render_time(None, t) + times_s = ", ".join([abbreviate_time(t) for t in per_server[peerid]]) ul(tags.li("[%s]: %s" % (peerid_s, times_s))) return tags.li("Per-Server Segment Fetch Response Times: ", ul) From ab4811eaa0c5ad48d1920bac90fceeb973d29407 Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Thu, 14 May 2020 15:57:59 -0400 Subject: [PATCH 014/205] Move tests for abbreviate time/rate methods to utils tests RateAndTimeMixin.render_time() is really abbreviate_time(), and RateAndTimeMixin.render_rate() is really abbreviate_rate(). This change moves the tests to their rightful place, and exercises them using the right names. We're also trying to avoid use of mixins. RateAndTimeMixin will eventually go. --- src/allmydata/test/web/test_util.py | 6 ++++++ src/allmydata/test/web/test_web.py | 11 ----------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/src/allmydata/test/web/test_util.py b/src/allmydata/test/web/test_util.py index 2a6f67abc..0e75b0d70 100644 --- a/src/allmydata/test/web/test_util.py +++ b/src/allmydata/test/web/test_util.py @@ -22,6 +22,9 @@ class Util(ShouldFailMixin, testutil.ReallyEqualMixin, unittest.TestCase): self.failUnlessReallyEqual(common.abbreviate_time(0.00123), "1.2ms") self.failUnlessReallyEqual(common.abbreviate_time(0.000123), "123us") self.failUnlessReallyEqual(common.abbreviate_time(-123000), "-123000000000us") + self.failUnlessReallyEqual(common.abbreviate_time(2.5), "2.50s") + self.failUnlessReallyEqual(common.abbreviate_time(0.25), "250ms") + self.failUnlessReallyEqual(common.abbreviate_time(0.0021), "2.1ms") def test_compute_rate(self): self.failUnlessReallyEqual(common.compute_rate(None, None), None) @@ -44,6 +47,9 @@ class Util(ShouldFailMixin, testutil.ReallyEqualMixin, unittest.TestCase): self.failUnlessReallyEqual(common.abbreviate_rate(1234000), "1.23MBps") self.failUnlessReallyEqual(common.abbreviate_rate(12340), "12.3kBps") self.failUnlessReallyEqual(common.abbreviate_rate(123), "123Bps") + self.failUnlessReallyEqual(common.abbreviate_rate(2500000), "2.50MBps") + self.failUnlessReallyEqual(common.abbreviate_rate(30100), "30.1kBps") + self.failUnlessReallyEqual(common.abbreviate_rate(123), "123Bps") def test_abbreviate_size(self): self.failUnlessReallyEqual(common.abbreviate_size(None), "") diff --git a/src/allmydata/test/web/test_web.py b/src/allmydata/test/web/test_web.py index 5be0b2f7b..d7b606263 100644 --- a/src/allmydata/test/web/test_web.py +++ b/src/allmydata/test/web/test_web.py @@ -1035,17 +1035,6 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, testutil.ReallyEqualMixi return d def test_status_numbers(self): - drrm = status.DownloadResultsRendererMixin() - self.failUnlessReallyEqual(drrm.render_time(None, None), "") - self.failUnlessReallyEqual(drrm.render_time(None, 2.5), "2.50s") - self.failUnlessReallyEqual(drrm.render_time(None, 0.25), "250ms") - self.failUnlessReallyEqual(drrm.render_time(None, 0.0021), "2.1ms") - self.failUnlessReallyEqual(drrm.render_time(None, 0.000123), "123us") - self.failUnlessReallyEqual(drrm.render_rate(None, None), "") - self.failUnlessReallyEqual(drrm.render_rate(None, 2500000), "2.50MBps") - self.failUnlessReallyEqual(drrm.render_rate(None, 30100), "30.1kBps") - self.failUnlessReallyEqual(drrm.render_rate(None, 123), "123Bps") - urrm = status.UploadResultsRendererMixin() self.failUnlessReallyEqual(urrm.render_time(None, None), "") self.failUnlessReallyEqual(urrm.render_time(None, 2.5), "2.50s") From 19e40e2193dd0f0268899a068e12774de7fc41f1 Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Thu, 14 May 2020 16:04:26 -0400 Subject: [PATCH 015/205] Add newsfragment --- newsfragments/3288.minor | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 newsfragments/3288.minor diff --git a/newsfragments/3288.minor b/newsfragments/3288.minor new file mode 100644 index 000000000..e69de29bb From c5342d8ae68acfcdf319ee079f7061ed5528bc20 Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Thu, 14 May 2020 16:48:43 -0400 Subject: [PATCH 016/205] Add comments about events tables --- src/allmydata/web/status.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/allmydata/web/status.py b/src/allmydata/web/status.py index 3d8ecb975..4916030d2 100644 --- a/src/allmydata/web/status.py +++ b/src/allmydata/web/status.py @@ -431,6 +431,7 @@ class DownloadStatusElement(Element): evtag = tags.div() + # "DYHB Requests" table. dyhbtag = tags.table(align="left", class_="status-download-events") dyhbtag(tags.tr(tags.th("serverid"), @@ -461,6 +462,7 @@ class DownloadStatusElement(Element): evtag(tags.h2("DYHB Requests:"), dyhbtag) evtag(tags.br(clear="all")) + # "Read Events" table. readtag = tags.table(align="left",class_="status-download-events") readtag(tags.tr(( @@ -501,6 +503,7 @@ class DownloadStatusElement(Element): evtag(tags.h2("Read Events:"), readtag) evtag(tags.br(clear="all")) + # "Segment Events" table. segtag = tags.table(align="left",class_="status-download-events") segtag(tags.tr( @@ -546,6 +549,7 @@ class DownloadStatusElement(Element): evtag(tags.h2("Segment Events:"), segtag) evtag(tags.br(clear="all")) + # "Requests" table. reqtab = tags.table(align="left",class_="status-download-events") reqtab(tags.tr( From 993b1e225f6a9038d28d98b601a5ebbb692b2205 Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Mon, 18 May 2020 11:01:14 -0400 Subject: [PATCH 017/205] Use transparent tags to render values --- src/allmydata/web/download-status.xhtml | 44 ++++++++++++------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/allmydata/web/download-status.xhtml b/src/allmydata/web/download-status.xhtml index 93bd08408..14fbb4b9e 100644 --- a/src/allmydata/web/download-status.xhtml +++ b/src/allmydata/web/download-status.xhtml @@ -12,12 +12,12 @@

File Download Status

    -
  • Started:
  • -
  • Storage Index:
  • -
  • Helper?:
  • -
  • Total Size:
  • -
  • Progress:
  • -
  • Status:
  • +
  • Started:
  • +
  • Storage Index:
  • +
  • Helper?:
  • +
  • Total Size:
  • +
  • Progress:
  • +
  • Status:
@@ -28,28 +28,28 @@
  • -
  • Servermap:
  • +
  • Servermap:
  • Timings:
    • -
    • File Size: bytes
    • -
    • Total: - ()
    • +
    • File Size: bytes
    • +
    • Total: + ()
      • -
      • Peer Selection:
      • -
      • UEB Fetch:
      • -
      • Hashtree Fetch:
      • -
      • Segment Fetch: - ()
      • +
      • Peer Selection:
      • +
      • UEB Fetch:
      • +
      • Hashtree Fetch:
      • +
      • Segment Fetch: + ()
        • -
        • Cumulative Fetching: - ()
        • -
        • Cumulative Decoding: - ()
        • -
        • Cumulative Decrypting: - ()
        • +
        • Cumulative Fetching: + ()
        • +
        • Cumulative Decoding: + ()
        • +
        • Cumulative Decrypting: + ()
        -
      • Paused by client:
      • +
      • Paused by client:
    From 0b3db59742ae727c682bb8a4f42022ef9fc4d6f5 Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Mon, 18 May 2020 13:30:39 -0400 Subject: [PATCH 018/205] Add related ticket to comment about unimplemented method --- src/allmydata/web/status.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/allmydata/web/status.py b/src/allmydata/web/status.py index 4916030d2..c9fa4f1f0 100644 --- a/src/allmydata/web/status.py +++ b/src/allmydata/web/status.py @@ -396,6 +396,8 @@ class DownloadStatusElement(Element): # `results()` renderer returns an empty tag, and does not invoke # any of the subsequent renderers. Thus we end up not displaying # download results on the download status page. + # + # See #3310: https://tahoe-lafs.org/trac/tahoe-lafs/ticket/3310 def download_results(self): return defer.maybeDeferred(self._download_status.get_results) From c23fedbcfeec5d465e2c9719f6b42a6ade4e1fa1 Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Mon, 18 May 2020 13:36:30 -0400 Subject: [PATCH 019/205] Add related ticket to comment about refactoring --- src/allmydata/web/status.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/allmydata/web/status.py b/src/allmydata/web/status.py index c9fa4f1f0..e6e422f6f 100644 --- a/src/allmydata/web/status.py +++ b/src/allmydata/web/status.py @@ -424,6 +424,8 @@ class DownloadStatusElement(Element): # XXX: This method is a candidate for refactoring. It renders # four tables from this function. Layout part of those tables # could be moved to download-status.xhtml. + # + # See #3311: https://tahoe-lafs.org/trac/tahoe-lafs/ticket/3311 @renderer def events(self, req, tag): if not self._download_status.storage_index: From aec6e5a9d4ca9f1a8bb8416e43642119b2160e45 Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Mon, 18 May 2020 20:03:58 -0400 Subject: [PATCH 020/205] Force a CircleCI build --- src/allmydata/web/download-status.xhtml | 1 - 1 file changed, 1 deletion(-) diff --git a/src/allmydata/web/download-status.xhtml b/src/allmydata/web/download-status.xhtml index 14fbb4b9e..0c2da523c 100644 --- a/src/allmydata/web/download-status.xhtml +++ b/src/allmydata/web/download-status.xhtml @@ -59,5 +59,4 @@
    Return to the Welcome Page
    - From 9520ad71eb3c6ffa5d42b80d525f26936e79163b Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Fri, 22 May 2020 21:24:21 -0400 Subject: [PATCH 021/205] Rearrange imports --- src/allmydata/web/status.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/allmydata/web/status.py b/src/allmydata/web/status.py index b20aa679e..327d610a6 100644 --- a/src/allmydata/web/status.py +++ b/src/allmydata/web/status.py @@ -1,5 +1,7 @@ -import pprint, itertools, hashlib +import pprint +import itertools +import hashlib import json from twisted.internet import defer from twisted.python.filepath import FilePath @@ -24,8 +26,14 @@ from allmydata.web.common import ( MultiFormatPage, MultiFormatResource, ) -from allmydata.interfaces import IUploadStatus, IDownloadStatus, \ - IPublishStatus, IRetrieveStatus, IServermapUpdaterStatus + +from allmydata.interfaces import ( + IUploadStatus, + IDownloadStatus, + IPublishStatus, + IRetrieveStatus, + IServermapUpdaterStatus, +) class RateAndTimeMixin(object): From 81dc63b511704269b2038e7113b3ce753671512a Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Fri, 22 May 2020 21:32:44 -0400 Subject: [PATCH 022/205] Use twisted template tags in status page template --- src/allmydata/web/status.xhtml | 44 +++++++++++++++++----------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/allmydata/web/status.xhtml b/src/allmydata/web/status.xhtml index d827002f4..045d5aa85 100644 --- a/src/allmydata/web/status.xhtml +++ b/src/allmydata/web/status.xhtml @@ -1,4 +1,4 @@ - + Tahoe-LAFS - Recent and Active Operations @@ -11,8 +11,8 @@

    Active Operations:

    - - +
    + @@ -20,21 +20,21 @@ - - - - - - - + + + + + + + - +
    Type Storage Index Helper?Progress Status
    No active operations!
    No active operations!

    Recent Operations:

    - - +
    + @@ -43,16 +43,16 @@ - - - - - - - - + + + + + + + + - +
    Started Type Storage IndexProgress Status
    No recent operations!
    No recent operations!

    From 3357f4c5ceea5a0dcde94d0ff069914821dbde61 Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Fri, 22 May 2020 21:43:41 -0400 Subject: [PATCH 023/205] Make Status a MultiFormatResource --- src/allmydata/web/status.py | 89 ++++++++++++++++++++++--------------- 1 file changed, 53 insertions(+), 36 deletions(-) diff --git a/src/allmydata/web/status.py b/src/allmydata/web/status.py index 327d610a6..e5165a570 100644 --- a/src/allmydata/web/status.py +++ b/src/allmydata/web/status.py @@ -1125,14 +1125,19 @@ def marshal_json(s): return item -class Status(MultiFormatPage): - docFactory = getxmlfile("status.xhtml") +class Status(MultiFormatResource): + addSlash = True def __init__(self, history): - rend.Page.__init__(self, history) + super(Status, self).__init__() self.history = history + def render_HTML(self, req): + elem = StatusElement(self._get_active_operations(), + self._get_recent_operations()) + return renderElement(req, elem) + def render_JSON(self, req): # modern browsers now render this instead of forcing downloads req.setHeader("content-type", "application/json") @@ -1180,7 +1185,51 @@ class Status(MultiFormatPage): recent.reverse() return recent - def render_row(self, ctx, data): + def childFactory(self, ctx, name): + h = self.history + try: + stype, count_s = name.split("-") + except ValueError: + raise RuntimeError( + "no - in '{}'".format(name) + ) + count = int(count_s) + if stype == "up": + for s in itertools.chain(h.list_all_upload_statuses(), + h.list_all_helper_statuses()): + # immutable-upload helpers use the same status object as a + # regular immutable-upload + if s.get_counter() == count: + return UploadStatusPage(s) + if stype == "down": + for s in h.list_all_download_statuses(): + if s.get_counter() == count: + return DownloadStatusPage(s) + if stype == "mapupdate": + for s in h.list_all_mapupdate_statuses(): + if s.get_counter() == count: + return MapupdateStatusPage(s) + if stype == "publish": + for s in h.list_all_publish_statuses(): + if s.get_counter() == count: + return PublishStatusPage(s) + if stype == "retrieve": + for s in h.list_all_retrieve_statuses(): + if s.get_counter() == count: + return RetrieveStatusPage(s) + + +class StatusElement(Element): + + loader = XMLFile(FilePath(__file__).sibling("status.xhtml")) + + def __init__(self, active, recent): + super(StatusElement, self).__init__() + self._active = active + self._recent = recent + + @renderer + def row(self, req, tag): s = data started_s = render_time(s.get_started()) @@ -1231,38 +1280,6 @@ class Status(MultiFormatPage): ctx.fillSlots("status", T.a(href=link)[s.get_status()]) return ctx.tag - def childFactory(self, ctx, name): - h = self.history - try: - stype, count_s = name.split("-") - except ValueError: - raise RuntimeError( - "no - in '{}'".format(name) - ) - count = int(count_s) - if stype == "up": - for s in itertools.chain(h.list_all_upload_statuses(), - h.list_all_helper_statuses()): - # immutable-upload helpers use the same status object as a - # regular immutable-upload - if s.get_counter() == count: - return UploadStatusPage(s) - if stype == "down": - for s in h.list_all_download_statuses(): - if s.get_counter() == count: - return DownloadStatusPage(s) - if stype == "mapupdate": - for s in h.list_all_mapupdate_statuses(): - if s.get_counter() == count: - return MapupdateStatusPage(s) - if stype == "publish": - for s in h.list_all_publish_statuses(): - if s.get_counter() == count: - return PublishStatusPage(s) - if stype == "retrieve": - for s in h.list_all_retrieve_statuses(): - if s.get_counter() == count: - return RetrieveStatusPage(s) # Render "/helper_status" page. class HelperStatus(MultiFormatResource): From a6e32135e866c8bb3621ae725137cb27953128df Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Fri, 22 May 2020 22:10:40 -0400 Subject: [PATCH 024/205] Update status page child route handler and table renderers Drop nevow-isms and use twisted.web's way of doing things. --- src/allmydata/web/status.py | 162 ++++++++++++++++++++---------------- 1 file changed, 90 insertions(+), 72 deletions(-) diff --git a/src/allmydata/web/status.py b/src/allmydata/web/status.py index e5165a570..1e0c81b98 100644 --- a/src/allmydata/web/status.py +++ b/src/allmydata/web/status.py @@ -25,6 +25,7 @@ from allmydata.web.common import ( render_time, MultiFormatPage, MultiFormatResource, + SlotsSequenceElement, ) from allmydata.interfaces import ( @@ -1127,8 +1128,6 @@ def marshal_json(s): class Status(MultiFormatResource): - addSlash = True - def __init__(self, history): super(Status, self).__init__() self.history = history @@ -1153,45 +1152,24 @@ class Status(MultiFormatResource): return json.dumps(data, indent=1) + "\n" - def _get_all_statuses(self): - h = self.history - return itertools.chain(h.list_all_upload_statuses(), - h.list_all_download_statuses(), - h.list_all_mapupdate_statuses(), - h.list_all_publish_statuses(), - h.list_all_retrieve_statuses(), - h.list_all_helper_statuses(), - ) + def getChild(self, path, request): + # The "if (path is empty) return self" line should handle + # trailing slash in request path. + # + # Twisted Web's documentation says this: "If the URL ends in a + # slash, for example ``http://example.com/foo/bar/`` , the + # final URL segment will be an empty string. Resources can + # thus know if they were requested with or without a final + # slash." + if not path: + return self - def data_active_operations(self, ctx, data): - return self._get_active_operations() - - def _get_active_operations(self): - active = [s - for s in self._get_all_statuses() - if s.get_active()] - active.sort(lambda a, b: cmp(a.get_started(), b.get_started())) - active.reverse() - return active - - def data_recent_operations(self, ctx, data): - return self._get_recent_operations() - - def _get_recent_operations(self): - recent = [s - for s in self._get_all_statuses() - if not s.get_active()] - recent.sort(lambda a, b: cmp(a.get_started(), b.get_started())) - recent.reverse() - return recent - - def childFactory(self, ctx, name): h = self.history try: - stype, count_s = name.split("-") + stype, count_s = path.split("-") except ValueError: raise RuntimeError( - "no - in '{}'".format(name) + "no - in '{}'".format(path) ) count = int(count_s) if stype == "up": @@ -1218,6 +1196,32 @@ class Status(MultiFormatResource): if s.get_counter() == count: return RetrieveStatusPage(s) + def _get_all_statuses(self): + h = self.history + return itertools.chain(h.list_all_upload_statuses(), + h.list_all_download_statuses(), + h.list_all_mapupdate_statuses(), + h.list_all_publish_statuses(), + h.list_all_retrieve_statuses(), + h.list_all_helper_statuses(), + ) + + def _get_active_operations(self): + active = [s + for s in self._get_all_statuses() + if s.get_active()] + active.sort(lambda a, b: cmp(a.get_started(), b.get_started())) + active.reverse() + return active + + def _get_recent_operations(self): + recent = [s + for s in self._get_all_statuses() + if not s.get_active()] + recent.sort(lambda a, b: cmp(a.get_started(), b.get_started())) + recent.reverse() + return recent + class StatusElement(Element): @@ -1229,56 +1233,70 @@ class StatusElement(Element): self._recent = recent @renderer - def row(self, req, tag): - s = data + def active_operations(self, req, tag): + active = [self.get_op_state(op) for op in self._active] + return SlotsSequenceElement(tag, active) - started_s = render_time(s.get_started()) - ctx.fillSlots("started", started_s) + @renderer + def recent_operations(self, req, tag): + active = [self.get_op_state(op) for op in self._recent] + return SlotsSequenceElement(tag, active) - si_s = base32.b2a_or_none(s.get_storage_index()) + @staticmethod + def get_op_state(op): + result = dict() + + started_s = render_time(op.get_started()) + result.update({"started": started_s}) + + si_s = base32.b2a_or_none(op.get_storage_index()) if si_s is None: si_s = "(None)" - ctx.fillSlots("si", si_s) - ctx.fillSlots("helper", {True: "Yes", - False: "No"}[s.using_helper()]) - size = s.get_size() + result.update({"si": si_s}) + result.update({"helper": + {True: "Yes", False: "No"}[op.using_helper()]}) + + size = op.get_size() if size is None: size = "(unknown)" elif isinstance(size, (int, long, float)): size = abbreviate_size(size) - ctx.fillSlots("total_size", size) - progress = data.get_progress() - if IUploadStatus.providedBy(data): - link = "up-%d" % data.get_counter() - ctx.fillSlots("type", "upload") + result.update({"total_size": size}) + + progress = op.get_progress() + if IUploadStatus.providedBy(op): + link = "up-%d" % op.get_counter() + result.update({"type": "upload"}) # TODO: make an ascii-art bar (chk, ciphertext, encandpush) = progress progress_s = ("hash: %.1f%%, ciphertext: %.1f%%, encode: %.1f%%" % - ( (100.0 * chk), - (100.0 * ciphertext), - (100.0 * encandpush) )) - ctx.fillSlots("progress", progress_s) - elif IDownloadStatus.providedBy(data): - link = "down-%d" % data.get_counter() - ctx.fillSlots("type", "download") - ctx.fillSlots("progress", "%.1f%%" % (100.0 * progress)) - elif IPublishStatus.providedBy(data): - link = "publish-%d" % data.get_counter() - ctx.fillSlots("type", "publish") - ctx.fillSlots("progress", "%.1f%%" % (100.0 * progress)) - elif IRetrieveStatus.providedBy(data): - ctx.fillSlots("type", "retrieve") - link = "retrieve-%d" % data.get_counter() - ctx.fillSlots("progress", "%.1f%%" % (100.0 * progress)) + ((100.0 * chk), + (100.0 * ciphertext), + (100.0 * encandpush))) + result.update({"progress": progress_s}) + elif IDownloadStatus.providedBy(op): + link = "down-%d" % op.get_counter() + result.update({"type": "download"}) + result.update({"progress": "%.1f%%" % (100.0 * progress)}) + elif IPublishStatus.providedBy(op): + link = "publish-%d" % op.get_counter() + result.update({"type": "publish"}) + result.update({"progress": "%.1f%%" % (100.0 * progress)}) + elif IRetrieveStatus.providedBy(op): + result.update({"type": "retrieve"}) + link = "retrieve-%d" % op.get_counter() + result.update({"progress": "%.1f%%" % (100.0 * progress)}) else: - assert IServermapUpdaterStatus.providedBy(data) - ctx.fillSlots("type", "mapupdate %s" % data.get_mode()) - link = "mapupdate-%d" % data.get_counter() - ctx.fillSlots("progress", "%.1f%%" % (100.0 * progress)) - ctx.fillSlots("status", T.a(href=link)[s.get_status()]) - return ctx.tag + assert IServermapUpdaterStatus.providedBy(op) + result.update({"type": "mapupdate %s" % op.get_mode()}) + link = "mapupdate-%d" % op.get_counter() + result.update({"progress": "%.1f%%" % (100.0 * progress)}) + + result.update({"status": tags.a(op.get_status(), href=link)}) + + return result # Render "/helper_status" page. From bd799d9d726953c08b82ff011c5f84b5d98d987a Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Fri, 22 May 2020 22:18:58 -0400 Subject: [PATCH 025/205] Add newsfragment --- newsfragments/3254.minor | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 newsfragments/3254.minor diff --git a/newsfragments/3254.minor b/newsfragments/3254.minor new file mode 100644 index 000000000..e69de29bb From 93d015e468dc427743de9dbf2cf35c67ca7ad189 Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Fri, 22 May 2020 22:24:09 -0400 Subject: [PATCH 026/205] Document status.Status --- src/allmydata/web/status.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/allmydata/web/status.py b/src/allmydata/web/status.py index 1e0c81b98..db127db59 100644 --- a/src/allmydata/web/status.py +++ b/src/allmydata/web/status.py @@ -1127,8 +1127,12 @@ def marshal_json(s): class Status(MultiFormatResource): + """Renders /status page.""" def __init__(self, history): + """ + :param allmydata.history.History history: provides operation statuses. + """ super(Status, self).__init__() self.history = history From d3f43d31d998fb97f8ad3e6fac007ed3fe57ab15 Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Sat, 23 May 2020 06:17:06 -0400 Subject: [PATCH 027/205] Remove unused import --- src/allmydata/web/status.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/allmydata/web/status.py b/src/allmydata/web/status.py index db127db59..ec18426dc 100644 --- a/src/allmydata/web/status.py +++ b/src/allmydata/web/status.py @@ -23,7 +23,6 @@ from allmydata.web.common import ( plural, compute_rate, render_time, - MultiFormatPage, MultiFormatResource, SlotsSequenceElement, ) From 6f8af688dddf865e607a313a234699415578d46e Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Mon, 1 Jun 2020 16:16:04 -0400 Subject: [PATCH 028/205] Rearrange imports --- src/allmydata/web/operations.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/allmydata/web/operations.py b/src/allmydata/web/operations.py index eb9321344..044a9fa64 100644 --- a/src/allmydata/web/operations.py +++ b/src/allmydata/web/operations.py @@ -12,8 +12,12 @@ from twisted.web.http import NOT_FOUND from twisted.web.html import escape from twisted.application import service -from allmydata.web.common import WebError, \ - get_root, get_arg, boolean_of_arg +from allmydata.web.common import ( + WebError, + get_root, + get_arg, + boolean_of_arg, +) MINUTE = 60 HOUR = 60*MINUTE From 5ec78641dbdd17a6717e7f682ee24e8a3b458d85 Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Thu, 18 Jun 2020 18:29:27 -0400 Subject: [PATCH 029/205] Make web.operations.OphandleTable a twisted web Resource --- src/allmydata/web/operations.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/allmydata/web/operations.py b/src/allmydata/web/operations.py index 044a9fa64..206542b06 100644 --- a/src/allmydata/web/operations.py +++ b/src/allmydata/web/operations.py @@ -1,13 +1,13 @@ import time -from nevow import rend, url -from nevow.inevow import IRequest +from nevow import url from twisted.web.template import ( renderer, tags as T, ) from twisted.python.failure import Failure from twisted.internet import reactor, defer +from twisted.web import resource from twisted.web.http import NOT_FOUND from twisted.web.html import escape from twisted.application import service @@ -25,13 +25,14 @@ DAY = 24*HOUR (MONITOR, RENDERER, WHEN_ADDED) = range(3) -class OphandleTable(rend.Page, service.Service): +class OphandleTable(resource.Resource, service.Service): name = "operations" UNCOLLECTED_HANDLE_LIFETIME = 4*DAY COLLECTED_HANDLE_LIFETIME = 1*DAY def __init__(self, clock=None): + super(OphandleTable, self).__init__() # both of these are indexed by ophandle self.handles = {} # tuple of (monitor, renderer, when_added) self.timers = {} @@ -80,27 +81,26 @@ class OphandleTable(rend.Page, service.Service): target = target + "?output=%s" % output return url.URL.fromString(target) - def childFactory(self, ctx, name): + def getChild(self, name, req): ophandle = name if ophandle not in self.handles: raise WebError("unknown/expired handle '%s'" % escape(ophandle), NOT_FOUND) (monitor, renderer, when_added) = self.handles[ophandle] - request = IRequest(ctx) - t = get_arg(ctx, "t", "status") - if t == "cancel" and request.method == "POST": + t = get_arg(req, "t", "status") + if t == "cancel" and req.method == "POST": monitor.cancel() # return the status anyways, but release the handle self._release_ophandle(ophandle) else: - retain_for = get_arg(ctx, "retain-for", None) + retain_for = get_arg(req, "retain-for", None) if retain_for is not None: self._set_timer(ophandle, int(retain_for)) if monitor.is_finished(): - if boolean_of_arg(get_arg(ctx, "release-after-complete", "false")): + if boolean_of_arg(get_arg(req, "release-after-complete", "false")): self._release_ophandle(ophandle) if retain_for is None: # this GET is collecting the ophandle, so change its timer @@ -127,6 +127,7 @@ class OphandleTable(rend.Page, service.Service): self.timers.pop(ophandle, None) self.handles.pop(ophandle, None) + class ReloadMixin(object): REFRESH_TIME = 1*MINUTE From bbee23b4937de49e6d331637c1d454425612256c Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Mon, 1 Jun 2020 18:19:08 -0400 Subject: [PATCH 030/205] Rename a parameter for clarity "context" is nevow terminology, whereas these are really requests. --- src/allmydata/web/operations.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/allmydata/web/operations.py b/src/allmydata/web/operations.py index 206542b06..75be2f975 100644 --- a/src/allmydata/web/operations.py +++ b/src/allmydata/web/operations.py @@ -50,12 +50,12 @@ class OphandleTable(resource.Resource, service.Service): del self.timers return service.Service.stopService(self) - def add_monitor(self, ctx, monitor, renderer): - ophandle = get_arg(ctx, "ophandle") + def add_monitor(self, req, monitor, renderer): + ophandle = get_arg(req, "ophandle") assert ophandle now = time.time() self.handles[ophandle] = (monitor, renderer, now) - retain_for = get_arg(ctx, "retain-for", None) + retain_for = get_arg(req, "retain-for", None) if retain_for is not None: self._set_timer(ophandle, int(retain_for)) monitor.when_done().addBoth(self._operation_complete, ophandle) @@ -72,11 +72,11 @@ class OphandleTable(resource.Resource, service.Service): # if we already have a timer, the client must have provided the # retain-for= value, so don't touch it. - def redirect_to(self, ctx): - ophandle = get_arg(ctx, "ophandle") + def redirect_to(self, req): + ophandle = get_arg(req, "ophandle") assert ophandle - target = get_root(ctx) + "/operations/" + ophandle - output = get_arg(ctx, "output") + target = get_root(req) + "/operations/" + ophandle + output = get_arg(req, "output") if output: target = target + "?output=%s" % output return url.URL.fromString(target) From 2d2e8a58724f88d63f329b08a934d407320f3277 Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Tue, 2 Jun 2020 08:49:41 -0400 Subject: [PATCH 031/205] Add comments to web.operations.OphandleTable --- src/allmydata/web/operations.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/allmydata/web/operations.py b/src/allmydata/web/operations.py index 75be2f975..857fdf2c9 100644 --- a/src/allmydata/web/operations.py +++ b/src/allmydata/web/operations.py @@ -26,6 +26,8 @@ DAY = 24*HOUR (MONITOR, RENDERER, WHEN_ADDED) = range(3) class OphandleTable(resource.Resource, service.Service): + """Renders /operations/%d.""" + name = "operations" UNCOLLECTED_HANDLE_LIFETIME = 4*DAY @@ -51,6 +53,11 @@ class OphandleTable(resource.Resource, service.Service): return service.Service.stopService(self) def add_monitor(self, req, monitor, renderer): + """ + :param allmydata.webish.MyRequest req: + :param allmydata.monitor.Monitor monitor: + :param allmydata.web.directory.ManifestResults renderer: + """ ophandle = get_arg(req, "ophandle") assert ophandle now = time.time() @@ -73,6 +80,9 @@ class OphandleTable(resource.Resource, service.Service): # retain-for= value, so don't touch it. def redirect_to(self, req): + """ + :param allmydata.webish.MyRequest req: + """ ophandle = get_arg(req, "ophandle") assert ophandle target = get_root(req) + "/operations/" + ophandle From 982ad8942e2417816ff91743001f3b5418d4b255 Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Thu, 18 Jun 2020 18:30:18 -0400 Subject: [PATCH 032/205] Comment about nevow.url usage in web.operations.OphandleTable --- src/allmydata/web/operations.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/allmydata/web/operations.py b/src/allmydata/web/operations.py index 857fdf2c9..4dcad0028 100644 --- a/src/allmydata/web/operations.py +++ b/src/allmydata/web/operations.py @@ -89,6 +89,10 @@ class OphandleTable(resource.Resource, service.Service): output = get_arg(req, "output") if output: target = target + "?output=%s" % output + + # XXX: We have to use nevow.url here because nevow.appserver + # is unhappy with anything else; so this gets its own ticket. + # https://tahoe-lafs.org/trac/tahoe-lafs/ticket/3314 return url.URL.fromString(target) def getChild(self, name, req): From df01c65540eb98168abd8c68bbb4a832f7509259 Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Tue, 2 Jun 2020 10:13:53 -0400 Subject: [PATCH 033/205] Add newsfragment --- newsfragments/3313.minor | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/3313.minor diff --git a/newsfragments/3313.minor b/newsfragments/3313.minor new file mode 100644 index 000000000..c4eecd956 --- /dev/null +++ b/newsfragments/3313.minor @@ -0,0 +1 @@ +Replace nevow with twisted.web in web.operations.OphandleTable From b14c0a0495020b0bf39bdb6a1804f2f0acf63184 Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Tue, 23 Jun 2020 18:06:54 -0400 Subject: [PATCH 034/205] Make DownloadStatusPage a Resource No need of this being a MultiFormatResource, because it renders just HTML. --- src/allmydata/web/status.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/allmydata/web/status.py b/src/allmydata/web/status.py index e6e422f6f..4f715fd5d 100644 --- a/src/allmydata/web/status.py +++ b/src/allmydata/web/status.py @@ -366,7 +366,7 @@ class _EventJson(Resource, object): return json.dumps(data, indent=1) + "\n" -class DownloadStatusPage(MultiFormatResource): +class DownloadStatusPage(Resource): """Renders /status/down-%d.""" def __init__(self, download_status): From 36ecd8f38455166ea2b275b2149361fdc4df6996 Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Tue, 23 Jun 2020 18:07:27 -0400 Subject: [PATCH 035/205] Use render_GET() Implementing `render_METHOD()` is preferred over overriding render() --- src/allmydata/web/status.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/allmydata/web/status.py b/src/allmydata/web/status.py index 4f715fd5d..2efd2a940 100644 --- a/src/allmydata/web/status.py +++ b/src/allmydata/web/status.py @@ -377,7 +377,7 @@ class DownloadStatusPage(Resource): self._download_status = download_status self.putChild("event_json", _EventJson(self._download_status)) - def render_HTML(self, req): + def render_GET(self, req): elem = DownloadStatusElement(self._download_status) return renderElement(req, elem) From 00e852c465ab211f449674932c0002da6afef6d4 Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Tue, 23 Jun 2020 20:51:26 -0400 Subject: [PATCH 036/205] Make DownloadStatus an object PythonTwoRegressions.test_new_style_classes would be unhappy without this. --- src/allmydata/web/status.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/allmydata/web/status.py b/src/allmydata/web/status.py index 2efd2a940..5100a9a3d 100644 --- a/src/allmydata/web/status.py +++ b/src/allmydata/web/status.py @@ -366,7 +366,7 @@ class _EventJson(Resource, object): return json.dumps(data, indent=1) + "\n" -class DownloadStatusPage(Resource): +class DownloadStatusPage(Resource, object): """Renders /status/down-%d.""" def __init__(self, download_status): From f9e864c51cf079a61cff449074f35ec3d7bc2a53 Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Wed, 24 Jun 2020 08:15:18 -0400 Subject: [PATCH 037/205] Use simpler syntax to update map --- src/allmydata/web/status.py | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/src/allmydata/web/status.py b/src/allmydata/web/status.py index ec18426dc..8214913de 100644 --- a/src/allmydata/web/status.py +++ b/src/allmydata/web/status.py @@ -1250,15 +1250,14 @@ class StatusElement(Element): result = dict() started_s = render_time(op.get_started()) - result.update({"started": started_s}) + result["started"] = started_s si_s = base32.b2a_or_none(op.get_storage_index()) if si_s is None: si_s = "(None)" - result.update({"si": si_s}) - result.update({"helper": - {True: "Yes", False: "No"}[op.using_helper()]}) + result["si"] = si_s + result["helper"] = {True: "Yes", False: "No"}[op.using_helper()] size = op.get_size() if size is None: @@ -1266,38 +1265,38 @@ class StatusElement(Element): elif isinstance(size, (int, long, float)): size = abbreviate_size(size) - result.update({"total_size": size}) + result["total_size"] = size progress = op.get_progress() if IUploadStatus.providedBy(op): link = "up-%d" % op.get_counter() - result.update({"type": "upload"}) + result["type"] = "upload" # TODO: make an ascii-art bar (chk, ciphertext, encandpush) = progress progress_s = ("hash: %.1f%%, ciphertext: %.1f%%, encode: %.1f%%" % ((100.0 * chk), (100.0 * ciphertext), (100.0 * encandpush))) - result.update({"progress": progress_s}) + result["progress"] = progress_s elif IDownloadStatus.providedBy(op): link = "down-%d" % op.get_counter() - result.update({"type": "download"}) - result.update({"progress": "%.1f%%" % (100.0 * progress)}) + result["type"] = "download" + result["progress"] = "%.1f%%" % (100.0 * progress) elif IPublishStatus.providedBy(op): link = "publish-%d" % op.get_counter() - result.update({"type": "publish"}) - result.update({"progress": "%.1f%%" % (100.0 * progress)}) + result["type"] = "publish" + result["progress"] = "%.1f%%" % (100.0 * progress) elif IRetrieveStatus.providedBy(op): - result.update({"type": "retrieve"}) + result["type"] = "retrieve" link = "retrieve-%d" % op.get_counter() - result.update({"progress": "%.1f%%" % (100.0 * progress)}) + result["progress"] = "%.1f%%" % (100.0 * progress) else: assert IServermapUpdaterStatus.providedBy(op) - result.update({"type": "mapupdate %s" % op.get_mode()}) + result["type"] = "mapupdate %s" % op.get_mode() link = "mapupdate-%d" % op.get_counter() - result.update({"progress": "%.1f%%" % (100.0 * progress)}) + result["progress"] = "%.1f%%" % (100.0 * progress) - result.update({"status": tags.a(op.get_status(), href=link)}) + result["status"] = tags.a(op.get_status(), href=link) return result From e6eb3877073ec97a37970b8632b46868abea99fc Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Mon, 6 Jul 2020 17:05:52 -0400 Subject: [PATCH 038/205] Remove status.RateAndTimeMixin Both UploadResultsRenderer and DownloadResultsRenderer do not use RateAndTimeMixin anymore: safe to remove it now. Tests for methods formerly in RateAndTimeMixin have been moved to test.web.test_util: specifically test_abbreviate_rate() and test_abbreviate_time(). --- src/allmydata/test/web/test_web.py | 23 ----------------------- src/allmydata/web/status.py | 8 -------- 2 files changed, 31 deletions(-) diff --git a/src/allmydata/test/web/test_web.py b/src/allmydata/test/web/test_web.py index 01cb6b1ca..eb7b0b2c1 100644 --- a/src/allmydata/test/web/test_web.py +++ b/src/allmydata/test/web/test_web.py @@ -1035,29 +1035,6 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, testutil.ReallyEqualMixi return d - def test_status_numbers(self): - urrm = status.UploadResultsRendererMixin() - self.failUnlessReallyEqual(urrm.render_time(None, None), "") - self.failUnlessReallyEqual(urrm.render_time(None, 2.5), "2.50s") - self.failUnlessReallyEqual(urrm.render_time(None, 0.25), "250ms") - self.failUnlessReallyEqual(urrm.render_time(None, 0.0021), "2.1ms") - self.failUnlessReallyEqual(urrm.render_time(None, 0.000123), "123us") - self.failUnlessReallyEqual(urrm.render_rate(None, None), "") - self.failUnlessReallyEqual(urrm.render_rate(None, 2500000), "2.50MBps") - self.failUnlessReallyEqual(urrm.render_rate(None, 30100), "30.1kBps") - self.failUnlessReallyEqual(urrm.render_rate(None, 123), "123Bps") - - drrm = status.DownloadResultsRendererMixin() - self.failUnlessReallyEqual(drrm.render_time(None, None), "") - self.failUnlessReallyEqual(drrm.render_time(None, 2.5), "2.50s") - self.failUnlessReallyEqual(drrm.render_time(None, 0.25), "250ms") - self.failUnlessReallyEqual(drrm.render_time(None, 0.0021), "2.1ms") - self.failUnlessReallyEqual(drrm.render_time(None, 0.000123), "123us") - self.failUnlessReallyEqual(drrm.render_rate(None, None), "") - self.failUnlessReallyEqual(drrm.render_rate(None, 2500000), "2.50MBps") - self.failUnlessReallyEqual(drrm.render_rate(None, 30100), "30.1kBps") - self.failUnlessReallyEqual(drrm.render_rate(None, 123), "123Bps") - def test_GET_FILEURL(self): d = self.GET(self.public_url + "/foo/bar.txt") d.addCallback(self.failUnlessIsBarDotTxt) diff --git a/src/allmydata/web/status.py b/src/allmydata/web/status.py index 38c9f8440..fe9043563 100644 --- a/src/allmydata/web/status.py +++ b/src/allmydata/web/status.py @@ -27,14 +27,6 @@ from allmydata.web.common import ( from allmydata.interfaces import IUploadStatus, IDownloadStatus, \ IPublishStatus, IRetrieveStatus, IServermapUpdaterStatus -class RateAndTimeMixin(object): - - def render_time(self, ctx, data): - return abbreviate_time(data) - - def render_rate(self, ctx, data): - return abbreviate_rate(data) - class UploadResultsRendererMixin(Element): # this requires a method named 'upload_results' From a3bf4e0269301eeb074ff8b8ee8efc367b111e74 Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Mon, 6 Jul 2020 20:18:22 -0400 Subject: [PATCH 039/205] Remove an unused import --- src/allmydata/test/web/test_web.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/allmydata/test/web/test_web.py b/src/allmydata/test/web/test_web.py index eb7b0b2c1..f68da2dae 100644 --- a/src/allmydata/test/web/test_web.py +++ b/src/allmydata/test/web/test_web.py @@ -33,7 +33,6 @@ from allmydata.immutable import upload from allmydata.immutable.downloader.status import DownloadStatus from allmydata.dirnode import DirectoryNode from allmydata.nodemaker import NodeMaker -from allmydata.web import status from allmydata.web.common import WebError, MultiFormatPage from allmydata.util import fileutil, base32, hashutil from allmydata.util.consumer import download_to_data From 7877d8a5cc9cfebddb48c76cd971632f9e9dac24 Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Tue, 7 Jul 2020 19:55:14 -0400 Subject: [PATCH 040/205] Use an accessor --- src/allmydata/web/status.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/allmydata/web/status.py b/src/allmydata/web/status.py index fe9043563..db11cc6e7 100644 --- a/src/allmydata/web/status.py +++ b/src/allmydata/web/status.py @@ -468,8 +468,8 @@ class DownloadStatusElement(Element): # See #3311: https://tahoe-lafs.org/trac/tahoe-lafs/ticket/3311 @renderer def events(self, req, tag): - if not self._download_status.storage_index: - return + if not self._download_status.get_storage_index(): + return tag srt = self._short_relative_time From 0c4d24a2ac8eba0d0995d207b214e0871c2307cf Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Tue, 7 Jul 2020 20:24:56 -0400 Subject: [PATCH 041/205] Avoid use of deferred within an Element This causes the final HTML to be rendered funny, with a bunch of "", which is clearly not what we want. --- src/allmydata/web/status.py | 121 ++++++++++++++---------------------- 1 file changed, 48 insertions(+), 73 deletions(-) diff --git a/src/allmydata/web/status.py b/src/allmydata/web/status.py index db11cc6e7..4a9594a1c 100644 --- a/src/allmydata/web/status.py +++ b/src/allmydata/web/status.py @@ -439,7 +439,7 @@ class DownloadStatusElement(Element): # # See #3310: https://tahoe-lafs.org/trac/tahoe-lafs/ticket/3310 def download_results(self): - return defer.maybeDeferred(self._download_status.get_results) + return self._download_status.get_results() def _relative_time(self, t): if t is None: @@ -629,13 +629,9 @@ class DownloadStatusElement(Element): @renderer def results(self, req, tag): - d = self.download_results() - def _got_results(results): - if results: - return tag - return "" - d.addCallback(_got_results) - return d + if self.download_results(): + return tag + return "" @renderer def started(self, req, tag): @@ -673,61 +669,47 @@ class DownloadStatusElement(Element): @renderer def servers_used(self, req, tag): - d = self.download_results() - d.addCallback(lambda res: res.servers_used) - def _got(servers_used): - if not servers_used: - return "" - peerids_s = ", ".join(["[%s]" % idlib.shortnodeid_b2a(peerid) - for peerid in servers_used]) - return tags.li("Servers Used: ", peerids_s) - d.addCallback(_got) - return d + servers_used = self.download_results().servers_used + + if not servers_used: + return "" + + peerids_s = ", ".join(["[%s]" % idlib.shortnodeid_b2a(peerid) + for peerid in servers_used]) + return tags.li("Servers Used: ", peerids_s) @renderer def servermap(self, req, tag): - d = self.download_results() - d.addCallback(lambda res: res.servermap) - def _render(servermap): - if servermap is None: - return tag("None") - ul = tags.ul() - for peerid in sorted(servermap.keys()): - peerid_s = idlib.shortnodeid_b2a(peerid) - shares_s = ",".join(["#%d" % shnum - for shnum in servermap[peerid]]) - ul(tags.li("[%s] has share%s: %s" % (peerid_s, - plural(servermap[peerid]), - shares_s))) - return ul - d.addCallback(_render) - return d + servermap = self.download_results().servermap + if not servermap: + return tag("None") + ul = tags.ul() + for peerid in sorted(servermap.keys()): + peerid_s = idlib.shortnodeid_b2a(peerid) + shares_s = ",".join(["#%d" % shnum + for shnum in servermap[peerid]]) + ul(tags.li("[%s] has share%s: %s" % (peerid_s, + plural(servermap[peerid]), + shares_s))) + return ul @renderer def problems(self, req, tag): - d = self.download_results() - d.addCallback(lambda res: res.server_problems) - def _got(server_problems): - if not server_problems: - return tag("") - ul = T.ul() - for peerid in sorted(server_problems.keys()): - peerid_s = idlib.shortnodeid_b2a(peerid) - ul(tags.li("[%s]: %s" % (peerid_s, server_problems[peerid]))) - return tags.li("Server Problems:", ul) - d.addCallback(_got) - return d + server_problems = self.download_results().server_problems + if not server_problems: + return "" + ul = T.ul() + for peerid in sorted(server_problems.keys()): + peerid_s = idlib.shortnodeid_b2a(peerid) + ul(tags.li("[%s]: %s" % (peerid_s, server_problems[peerid]))) + return tags.li("Server Problems:", ul) @renderer def file_size(self, req, tag): - d = self.download_results() - d.addCallback(lambda res: str(res.file_size)) - return d + return tag(str(self.download_results().file_size)) def _get_time(self, name): - d = self.download_results() - d.addCallback(lambda res: res.timings.get(name)) - return d + return self.download_results().timings.get(name) @renderer def time_total(self, req, tag): @@ -766,13 +748,10 @@ class DownloadStatusElement(Element): return tag(str(self._get_time("paused"))) def _get_rate(self, name): - d = self.download_results() - def _convert(r): - file_size = r.file_size - duration = r.timings.get(name) - return compute_rate(file_size, duration) - d.addCallback(_convert) - return d + r = self.download_results() + file_size = r.file_size + duration = r.timings.get(name) + return compute_rate(file_size, duration) @renderer def rate_total(self, req, tag): @@ -796,20 +775,16 @@ class DownloadStatusElement(Element): @renderer def server_timings(self, req, tag): - d = self.download_results() - d.addCallback(lambda res: res.timings.get("fetch_per_server")) - def _render(per_server): - if per_server is None: - return "" - ul = tags.ul() - for peerid in sorted(per_server.keys()): - peerid_s = idlib.shortnodeid_b2a(peerid) - times_s = ", ".join([abbreviate_time(t) - for t in per_server[peerid]]) - ul(tags.li("[%s]: %s" % (peerid_s, times_s))) - return tags.li("Per-Server Segment Fetch Response Times: ", ul) - d.addCallback(_render) - return d + per_server = self._get_time("fetch_per_server") + if per_server is None: + return "" + ul = tags.ul() + for peerid in sorted(per_server.keys()): + peerid_s = idlib.shortnodeid_b2a(peerid) + times_s = ", ".join([abbreviate_time(t) + for t in per_server[peerid]]) + ul(tags.li("[%s]: %s" % (peerid_s, times_s))) + return tags.li("Per-Server Segment Fetch Response Times: ", ul) class RetrieveStatusPage(MultiFormatResource): From 7757756a418809ce69749ac48ccc266f9d3080cb Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Tue, 7 Jul 2020 20:45:30 -0400 Subject: [PATCH 042/205] Use correct tag type --- src/allmydata/web/status.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/allmydata/web/status.py b/src/allmydata/web/status.py index 4a9594a1c..94d35285b 100644 --- a/src/allmydata/web/status.py +++ b/src/allmydata/web/status.py @@ -698,7 +698,7 @@ class DownloadStatusElement(Element): server_problems = self.download_results().server_problems if not server_problems: return "" - ul = T.ul() + ul = tags.ul() for peerid in sorted(server_problems.keys()): peerid_s = idlib.shortnodeid_b2a(peerid) ul(tags.li("[%s]: %s" % (peerid_s, server_problems[peerid]))) From 0db56f99cf28f73aa616447c595d633f74177735 Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Tue, 7 Jul 2020 20:49:58 -0400 Subject: [PATCH 043/205] Add some tests for DownloadStatusElement --- src/allmydata/test/web/test_status.py | 82 +++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 src/allmydata/test/web/test_status.py diff --git a/src/allmydata/test/web/test_status.py b/src/allmydata/test/web/test_status.py new file mode 100644 index 000000000..bc7d46fc4 --- /dev/null +++ b/src/allmydata/test/web/test_status.py @@ -0,0 +1,82 @@ + +from bs4 import BeautifulSoup +from twisted.web.template import flattenString +from zope.interface import implementer + +from allmydata.interfaces import ( + IDownloadResults, + IDownloadStatus, +) +from allmydata.web.status import DownloadStatusElement + +from .common import ( + assert_soup_has_favicon, + assert_soup_has_text, + assert_soup_has_tag_with_content, +) +from ..common import TrialTestCase + + +@implementer(IDownloadResults) +class FakeDownloadResults(object): + file_size = 0 + servers_used = 0 + server_problems = {"s-1": "unknown problem"} + servermap = dict() + timings = dict() + + +@implementer(IDownloadStatus) +class FakeDownloadStatus(object): + + def __init__(self, storage_index, size): + self.storage_index = storage_index + self.size = size + self.dyhb_requests = [] + self.read_events = [] + self.segment_events = [] + self.block_requests = [] + + def get_started(self): + return None + + def get_storage_index(self): + return self.storage_index + + def get_size(self): + return self.size + + def using_helper(self): + return False + + def get_status(self): + return "FakeDownloadStatus" + + def get_progress(self): + return 0 + + def get_active(): + return False + + def get_counter(): + return 0 + + def get_results(self): + return FakeDownloadResults() + + +class StatusTests(TrialTestCase): + + def _render_download_status_element(self): + elem = DownloadStatusElement(FakeDownloadStatus("si-1", 123)) + d = flattenString(None, elem) + return self.successResultOf(d) + + def test_download_status_element(self): + result = self._render_download_status_element() + soup = BeautifulSoup(result, 'html5lib') + + assert_soup_has_text(self, soup, u'Tahoe-LAFS - File Download Status') + assert_soup_has_favicon(self, soup) + + assert_soup_has_tag_with_content(self, soup, u'li', u'[omwtc]: unknown problem') From d6ea30b9356c6dd792603551fb17398224601dec Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Tue, 7 Jul 2020 21:56:35 -0400 Subject: [PATCH 044/205] Test more markup rendered by DownloadStatusElement --- src/allmydata/test/web/test_status.py | 39 ++++++++++++++++++++------- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/src/allmydata/test/web/test_status.py b/src/allmydata/test/web/test_status.py index bc7d46fc4..a04b7cdd7 100644 --- a/src/allmydata/test/web/test_status.py +++ b/src/allmydata/test/web/test_status.py @@ -1,3 +1,4 @@ +# Tests for code in allmydata.web.status from bs4 import BeautifulSoup from twisted.web.template import flattenString @@ -19,11 +20,13 @@ from ..common import TrialTestCase @implementer(IDownloadResults) class FakeDownloadResults(object): - file_size = 0 - servers_used = 0 - server_problems = {"s-1": "unknown problem"} - servermap = dict() - timings = dict() + + def __init__(self, file_size): + self.file_size = file_size + self.servers_used = ["s-1", "s-2", "s-3"] + self.server_problems = {"s-1": "unknown problem"} + self.servermap = {"s-1": [1,2,3], "s-2": [2,3,4], "s-3": [0,1,3]} + self.timings = { "fetch_per_server": {"s-1": [1,2,3], "s-2": [2], "s-3": [3]}} @implementer(IDownloadStatus) @@ -62,10 +65,10 @@ class FakeDownloadStatus(object): return 0 def get_results(self): - return FakeDownloadResults() + return FakeDownloadResults(self.size) - -class StatusTests(TrialTestCase): +# Tests for code in allmydata.web.status.DownloadStatusElement +class DownloadStatusElementTests(TrialTestCase): def _render_download_status_element(self): elem = DownloadStatusElement(FakeDownloadStatus("si-1", 123)) @@ -76,7 +79,23 @@ class StatusTests(TrialTestCase): result = self._render_download_status_element() soup = BeautifulSoup(result, 'html5lib') - assert_soup_has_text(self, soup, u'Tahoe-LAFS - File Download Status') + assert_soup_has_text(self, soup, u"Tahoe-LAFS - File Download Status") assert_soup_has_favicon(self, soup) - assert_soup_has_tag_with_content(self, soup, u'li', u'[omwtc]: unknown problem') + assert_soup_has_tag_with_content(self, soup, u"li", u"File Size: 123 bytes") + assert_soup_has_tag_with_content(self, soup, u"li", u"Progress: 0.0%") + + assert_soup_has_tag_with_content(self, soup, u"li", u"Servers Used: [omwtc], [omwte], [omwtg]") + + assert_soup_has_tag_with_content(self, soup, u"li", u"Server Problems:") + assert_soup_has_tag_with_content(self, soup, u"li", u"[omwtc]: unknown problem") + + assert_soup_has_tag_with_content(self, soup, u"li", u"Servermap:") + assert_soup_has_tag_with_content(self, soup, u"li", u"[omwtc] has shares: #1,#2,#3") + assert_soup_has_tag_with_content(self, soup, u"li", u"[omwte] has shares: #2,#3,#4") + assert_soup_has_tag_with_content(self, soup, u"li", u"[omwtg] has shares: #0,#1,#3") + + assert_soup_has_tag_with_content(self, soup, u"li", u"Per-Server Segment Fetch Response Times:") + assert_soup_has_tag_with_content(self, soup, u"li", u"[omwtc]: 1.00s, 2.00s, 3.00s") + assert_soup_has_tag_with_content(self, soup, u"li", u"[omwte]: 2.00s") + assert_soup_has_tag_with_content(self, soup, u"li", u"[omwtg]: 3.00s") From 81216f18bf393f08216ed0b986ebfe5fbd581225 Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Wed, 8 Jul 2020 10:15:33 -0400 Subject: [PATCH 045/205] Remove some whitespace --- src/allmydata/test/web/test_status.py | 2 +- src/allmydata/web/status.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/allmydata/test/web/test_status.py b/src/allmydata/test/web/test_status.py index a04b7cdd7..15b641a8a 100644 --- a/src/allmydata/test/web/test_status.py +++ b/src/allmydata/test/web/test_status.py @@ -26,7 +26,7 @@ class FakeDownloadResults(object): self.servers_used = ["s-1", "s-2", "s-3"] self.server_problems = {"s-1": "unknown problem"} self.servermap = {"s-1": [1,2,3], "s-2": [2,3,4], "s-3": [0,1,3]} - self.timings = { "fetch_per_server": {"s-1": [1,2,3], "s-2": [2], "s-3": [3]}} + self.timings = {"fetch_per_server": {"s-1": [1,2,3], "s-2": [2], "s-3": [3]}} @implementer(IDownloadStatus) diff --git a/src/allmydata/web/status.py b/src/allmydata/web/status.py index 94d35285b..5d0b2a861 100644 --- a/src/allmydata/web/status.py +++ b/src/allmydata/web/status.py @@ -670,10 +670,8 @@ class DownloadStatusElement(Element): @renderer def servers_used(self, req, tag): servers_used = self.download_results().servers_used - if not servers_used: return "" - peerids_s = ", ".join(["[%s]" % idlib.shortnodeid_b2a(peerid) for peerid in servers_used]) return tags.li("Servers Used: ", peerids_s) From c187e2752b1fc455e440524d376a4b51edf43038 Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Wed, 8 Jul 2020 12:44:09 -0400 Subject: [PATCH 046/205] Parameterize FakeDownloadResults and FakeDownloadStatus --- src/allmydata/test/web/test_status.py | 75 ++++++++++++++++++++------- 1 file changed, 57 insertions(+), 18 deletions(-) diff --git a/src/allmydata/test/web/test_status.py b/src/allmydata/test/web/test_status.py index 15b641a8a..4366d88ea 100644 --- a/src/allmydata/test/web/test_status.py +++ b/src/allmydata/test/web/test_status.py @@ -21,25 +21,47 @@ from ..common import TrialTestCase @implementer(IDownloadResults) class FakeDownloadResults(object): - def __init__(self, file_size): + def __init__(self, + file_size=0, + servers_used=None, + server_problems=None, + servermap=None, + timings=None): + """ + See IDownloadResults for parameters. + """ self.file_size = file_size - self.servers_used = ["s-1", "s-2", "s-3"] - self.server_problems = {"s-1": "unknown problem"} - self.servermap = {"s-1": [1,2,3], "s-2": [2,3,4], "s-3": [0,1,3]} - self.timings = {"fetch_per_server": {"s-1": [1,2,3], "s-2": [2], "s-3": [3]}} + self.servers_used = servers_used + self.server_problems = server_problems + self.servermap = servermap + self.timings = timings @implementer(IDownloadStatus) class FakeDownloadStatus(object): - def __init__(self, storage_index, size): + def __init__(self, + storage_index = None, + file_size = 0, + servers_used = None, + server_problems = None, + servermap = None, + timings = None): + """ + See IDownloadStatus and IDownloadResults for parameters. + """ self.storage_index = storage_index - self.size = size + self.file_size = file_size self.dyhb_requests = [] self.read_events = [] self.segment_events = [] self.block_requests = [] + self.servers_used = servers_used + self.server_problems = server_problems + self.servermap = servermap + self.timings = timings + def get_started(self): return None @@ -47,7 +69,7 @@ class FakeDownloadStatus(object): return self.storage_index def get_size(self): - return self.size + return self.file_size def using_helper(self): return False @@ -65,18 +87,35 @@ class FakeDownloadStatus(object): return 0 def get_results(self): - return FakeDownloadResults(self.size) + return FakeDownloadResults(self.file_size, + self.servers_used, + self.server_problems, + self.servermap, + self.timings) # Tests for code in allmydata.web.status.DownloadStatusElement class DownloadStatusElementTests(TrialTestCase): - def _render_download_status_element(self): - elem = DownloadStatusElement(FakeDownloadStatus("si-1", 123)) + def _render_download_status_element(self, status): + """ + :param IDownloadStatus status: + :return: HTML string rendered by DownloadStatusElement + """ + elem = DownloadStatusElement(status) d = flattenString(None, elem) return self.successResultOf(d) def test_download_status_element(self): - result = self._render_download_status_element() + """ + See if we can render the page almost fully. + """ + status = FakeDownloadStatus("si-1", 123, + ["s-1", "s-2", "s-3"], + {"s-1": "unknown problem"}, + {"s-1": [1], "s-2": [1,2], "s-3": [2,3]}, + {"fetch_per_server": {"s-1": [1], "s-2": [2,3], "s-3": [3,2]}}) + + result = self._render_download_status_element(status) soup = BeautifulSoup(result, 'html5lib') assert_soup_has_text(self, soup, u"Tahoe-LAFS - File Download Status") @@ -91,11 +130,11 @@ class DownloadStatusElementTests(TrialTestCase): assert_soup_has_tag_with_content(self, soup, u"li", u"[omwtc]: unknown problem") assert_soup_has_tag_with_content(self, soup, u"li", u"Servermap:") - assert_soup_has_tag_with_content(self, soup, u"li", u"[omwtc] has shares: #1,#2,#3") - assert_soup_has_tag_with_content(self, soup, u"li", u"[omwte] has shares: #2,#3,#4") - assert_soup_has_tag_with_content(self, soup, u"li", u"[omwtg] has shares: #0,#1,#3") + assert_soup_has_tag_with_content(self, soup, u"li", u"[omwtc] has share: #1") + assert_soup_has_tag_with_content(self, soup, u"li", u"[omwte] has shares: #1,#2") + assert_soup_has_tag_with_content(self, soup, u"li", u"[omwtg] has shares: #2,#3") assert_soup_has_tag_with_content(self, soup, u"li", u"Per-Server Segment Fetch Response Times:") - assert_soup_has_tag_with_content(self, soup, u"li", u"[omwtc]: 1.00s, 2.00s, 3.00s") - assert_soup_has_tag_with_content(self, soup, u"li", u"[omwte]: 2.00s") - assert_soup_has_tag_with_content(self, soup, u"li", u"[omwtg]: 3.00s") + assert_soup_has_tag_with_content(self, soup, u"li", u"[omwtc]: 1.00s") + assert_soup_has_tag_with_content(self, soup, u"li", u"[omwte]: 2.00s, 3.00s") + assert_soup_has_tag_with_content(self, soup, u"li", u"[omwtg]: 3.00s, 2.00s") From 112cfc1da5278bd6896dbd58772178f0dd464a6b Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Wed, 8 Jul 2020 13:07:19 -0400 Subject: [PATCH 047/205] Test if download status page can be rendered partially --- src/allmydata/test/web/test_status.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/allmydata/test/web/test_status.py b/src/allmydata/test/web/test_status.py index 4366d88ea..e555c2229 100644 --- a/src/allmydata/test/web/test_status.py +++ b/src/allmydata/test/web/test_status.py @@ -138,3 +138,15 @@ class DownloadStatusElementTests(TrialTestCase): assert_soup_has_tag_with_content(self, soup, u"li", u"[omwtc]: 1.00s") assert_soup_has_tag_with_content(self, soup, u"li", u"[omwte]: 2.00s, 3.00s") assert_soup_has_tag_with_content(self, soup, u"li", u"[omwtg]: 3.00s, 2.00s") + + def test_download_status_element_partial(self): + """ + See if we can render the page with incomplete download status. + """ + status = FakeDownloadStatus() + result = self._render_download_status_element(status) + soup = BeautifulSoup(result, 'html5lib') + + assert_soup_has_tag_with_content(self, soup, u"li", u"Servermap: None") + assert_soup_has_tag_with_content(self, soup, u"li", u"File Size: 0 bytes") + assert_soup_has_tag_with_content(self, soup, u"li", u"Total: None (None)") From 51d6307b45ae62cf6040cb1faa590f8a0ccdfcf4 Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Wed, 8 Jul 2020 13:08:19 -0400 Subject: [PATCH 048/205] Fail better when timings data isn't available --- src/allmydata/web/status.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/allmydata/web/status.py b/src/allmydata/web/status.py index 5d0b2a861..b2e54f8cf 100644 --- a/src/allmydata/web/status.py +++ b/src/allmydata/web/status.py @@ -707,7 +707,9 @@ class DownloadStatusElement(Element): return tag(str(self.download_results().file_size)) def _get_time(self, name): - return self.download_results().timings.get(name) + if self.download_results().timings: + return self.download_results().timings.get(name) + return None @renderer def time_total(self, req, tag): @@ -748,7 +750,9 @@ class DownloadStatusElement(Element): def _get_rate(self, name): r = self.download_results() file_size = r.file_size - duration = r.timings.get(name) + duration = None + if r.timings: + duration = r.timings.get(name) return compute_rate(file_size, duration) @renderer From 4d22390b59e503169953f9ece4ac4e168b420a68 Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Wed, 8 Jul 2020 13:29:01 -0400 Subject: [PATCH 049/205] Untabify --- src/allmydata/test/web/test_status.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/allmydata/test/web/test_status.py b/src/allmydata/test/web/test_status.py index e555c2229..2c0fe032c 100644 --- a/src/allmydata/test/web/test_status.py +++ b/src/allmydata/test/web/test_status.py @@ -31,10 +31,10 @@ class FakeDownloadResults(object): See IDownloadResults for parameters. """ self.file_size = file_size - self.servers_used = servers_used - self.server_problems = server_problems - self.servermap = servermap - self.timings = timings + self.servers_used = servers_used + self.server_problems = server_problems + self.servermap = servermap + self.timings = timings @implementer(IDownloadStatus) From 7bee9ff54007b3a71a7905febf741e69e026165f Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Wed, 8 Jul 2020 13:56:52 -0400 Subject: [PATCH 050/205] Untabify again --- src/allmydata/test/web/test_status.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/allmydata/test/web/test_status.py b/src/allmydata/test/web/test_status.py index 2c0fe032c..42f2b9794 100644 --- a/src/allmydata/test/web/test_status.py +++ b/src/allmydata/test/web/test_status.py @@ -30,7 +30,7 @@ class FakeDownloadResults(object): """ See IDownloadResults for parameters. """ - self.file_size = file_size + self.file_size = file_size self.servers_used = servers_used self.server_problems = server_problems self.servermap = servermap From 54248f334ed8a7eaab0df7ffa6e5ce981e4187d5 Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Thu, 9 Jul 2020 10:01:13 -0400 Subject: [PATCH 051/205] Use WebError, not RuntimeError, to indicate error Several problems with using RuntimeError to signal error here: - It dumps a rather unhelpful webpage at the user. - The exception backtrace on Tahoe console is not quite necessary here. - It really is not a runtime error: it is just an expected failure. - But mainly, testing for RuntimeError is harder. --- src/allmydata/web/status.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/allmydata/web/status.py b/src/allmydata/web/status.py index 8214913de..0c9e1ebb1 100644 --- a/src/allmydata/web/status.py +++ b/src/allmydata/web/status.py @@ -25,6 +25,7 @@ from allmydata.web.common import ( render_time, MultiFormatResource, SlotsSequenceElement, + WebError, ) from allmydata.interfaces import ( @@ -1171,9 +1172,7 @@ class Status(MultiFormatResource): try: stype, count_s = path.split("-") except ValueError: - raise RuntimeError( - "no - in '{}'".format(path) - ) + raise WebError("no '-' in '{}'".format(path)) count = int(count_s) if stype == "up": for s in itertools.chain(h.list_all_upload_statuses(), From 2e160c15266284d84e1e94b9025814b0206fe0a1 Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Thu, 9 Jul 2020 14:23:26 -0400 Subject: [PATCH 052/205] Test for failure caused by invalid path A hyphen is expected when rendering /status page child nodes: "/status/up" is wrong; "/status/up-0" is right. --- src/allmydata/test/web/test_web.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/allmydata/test/web/test_web.py b/src/allmydata/test/web/test_web.py index 5be0b2f7b..c3779c0be 100644 --- a/src/allmydata/test/web/test_web.py +++ b/src/allmydata/test/web/test_web.py @@ -1034,6 +1034,11 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, testutil.ReallyEqualMixi return d + def test_status_path_error(self): + # Expect an error, because path is expected to be of the form + # "/status/{up,down,..}-%number", with a hyphen. + return self.assertFailure(self.GET("/storage/nodash"), error.Error) + def test_status_numbers(self): drrm = status.DownloadResultsRendererMixin() self.failUnlessReallyEqual(drrm.render_time(None, None), "") From 4dd6c8695076ce579f96f3af01116bdd6c0694e0 Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Tue, 2 Jun 2020 11:56:59 -0400 Subject: [PATCH 053/205] Use twisted web directives in web.operations.ReloadMixin --- src/allmydata/web/operations.py | 36 ++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/src/allmydata/web/operations.py b/src/allmydata/web/operations.py index 93de2169e..eb9321344 100644 --- a/src/allmydata/web/operations.py +++ b/src/allmydata/web/operations.py @@ -1,7 +1,11 @@ import time -from nevow import rend, url, tags as T +from nevow import rend, url from nevow.inevow import IRequest +from twisted.web.template import ( + renderer, + tags as T, +) from twisted.python.failure import Failure from twisted.internet import reactor, defer from twisted.web.http import NOT_FOUND @@ -122,31 +126,31 @@ class OphandleTable(rend.Page, service.Service): class ReloadMixin(object): REFRESH_TIME = 1*MINUTE - def render_refresh(self, ctx, data): + @renderer + def refresh(self, req, tag): if self.monitor.is_finished(): return "" # dreid suggests ctx.tag(**dict([("http-equiv", "refresh")])) # but I can't tell if he's joking or not - ctx.tag.attributes["http-equiv"] = "refresh" - ctx.tag.attributes["content"] = str(self.REFRESH_TIME) - return ctx.tag + tag.attributes["http-equiv"] = "refresh" + tag.attributes["content"] = str(self.REFRESH_TIME) + return tag - def render_reload(self, ctx, data): + @renderer + def reload(self, req, tag): if self.monitor.is_finished(): return "" - req = IRequest(ctx) # url.gethere would break a proxy, so the correct thing to do is # req.path[-1] + queryargs ophandle = req.prepath[-1] reload_target = ophandle + "?output=html" cancel_target = ophandle + "?t=cancel" - cancel_button = T.form(action=cancel_target, method="POST", - enctype="multipart/form-data")[ - T.input(type="submit", value="Cancel"), - ] + cancel_button = T.form(T.input(type="submit", value="Cancel"), + action=cancel_target, + method="POST", + enctype="multipart/form-data",) - return [T.h2["Operation still running: ", - T.a(href=reload_target)["Reload"], - ], - cancel_button, - ] + return (T.h2("Operation still running: ", + T.a("Reload", href=reload_target), + ), + cancel_button,) From c043d1beadde39dc79385729c8603ba2117b2926 Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Tue, 2 Jun 2020 12:06:48 -0400 Subject: [PATCH 054/205] Add newsfragment --- newsfragments/3315.minor | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/3315.minor diff --git a/newsfragments/3315.minor b/newsfragments/3315.minor new file mode 100644 index 000000000..ed00922a6 --- /dev/null +++ b/newsfragments/3315.minor @@ -0,0 +1 @@ +Add your info here \ No newline at end of file From 88accc23c3c0614a2f1d09ee10467a9cc7d56b0e Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Fri, 5 Jun 2020 09:28:36 -0400 Subject: [PATCH 055/205] Update newsfragment --- newsfragments/3315.minor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/newsfragments/3315.minor b/newsfragments/3315.minor index ed00922a6..0536c297a 100644 --- a/newsfragments/3315.minor +++ b/newsfragments/3315.minor @@ -1 +1 @@ -Add your info here \ No newline at end of file +Replace nevow with twisted.web in web.operations.ReloadMixin From 7444d6b7a9a1c6ca71e6b499e304c1bb0f807f14 Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Thu, 28 May 2020 13:42:55 -0400 Subject: [PATCH 056/205] Correct arguments to super() This was incidentally found when looking at some test failures: super(type, object) is the right invocation, and not the other way around. --- src/allmydata/test/common.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/allmydata/test/common.py b/src/allmydata/test/common.py index e7a94ff40..a2af857b9 100644 --- a/src/allmydata/test/common.py +++ b/src/allmydata/test/common.py @@ -1269,5 +1269,5 @@ class TrialTestCase(_TrialTestCase): if six.PY2: if isinstance(msg, six.text_type): - return super(self, TrialTestCase).fail(msg.encode("utf8")) - return super(self, TrialTestCase).fail(msg) + return super(TrialTestCase, self).fail(msg.encode("utf8")) + return super(TrialTestCase, self).fail(msg) From 5892eae580a71d2b99760ec72685532df9a8cb45 Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Thu, 28 May 2020 13:49:13 -0400 Subject: [PATCH 057/205] Render self in DirectoryNodeHandler.getChild() We need self.render_POST() etc. to be invoked when we have a request such as "POST /uri/URI:DIR:..."; throwing an error here is probably not the right thing to do. --- src/allmydata/web/directory.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/allmydata/web/directory.py b/src/allmydata/web/directory.py index 57c222788..2129cec25 100644 --- a/src/allmydata/web/directory.py +++ b/src/allmydata/web/directory.py @@ -103,12 +103,10 @@ class DirectoryNodeHandler(ReplaceMeMixin, Resource, object): # for these nodes, which is that a URI like # "/uri/URI%3ADIR2%3Aj...vq/" (that is, with a trailing slash # or no further children) renders "this" page - name = name.decode('utf8') if not name: - raise EmptyPathnameComponentError( - u"The webapi does not allow empty pathname components", - ) + return self + name = name.decode('utf8') d = self.node.get(name) d.addBoth(self._got_child, req, name) return d From 83b5807ca119a4641ee1a5e928e6a10d3ef5d147 Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Fri, 29 May 2020 09:24:47 -0400 Subject: [PATCH 058/205] Add newsfragment --- newsfragments/3312.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/3312.bugfix diff --git a/newsfragments/3312.bugfix b/newsfragments/3312.bugfix new file mode 100644 index 000000000..ed00922a6 --- /dev/null +++ b/newsfragments/3312.bugfix @@ -0,0 +1 @@ +Add your info here \ No newline at end of file From d3d0d0285596ac75c055be5662fb7b9099630821 Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Fri, 29 May 2020 19:47:12 -0400 Subject: [PATCH 059/205] Reject URIs that contain empty segments --- src/allmydata/web/directory.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/allmydata/web/directory.py b/src/allmydata/web/directory.py index 2129cec25..fced8c68e 100644 --- a/src/allmydata/web/directory.py +++ b/src/allmydata/web/directory.py @@ -106,6 +106,16 @@ class DirectoryNodeHandler(ReplaceMeMixin, Resource, object): if not name: return self + # Rejecting URIs that contain empty path pieces (for example: + # "/uri/URI:DIR2:../foo//new.txt" or "/uri/URI:DIR2:..//") was + # the old nevow behavior and it is encoded in the test suite; + # we will follow suit. + for segment in req.prepath: + if not segment: + raise EmptyPathnameComponentError( + u"The webapi does not allow empty pathname components", + ) + name = name.decode('utf8') d = self.node.get(name) d.addBoth(self._got_child, req, name) From 378b20c51a489a22deb80567f4a18807c7ed55d4 Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Fri, 29 May 2020 21:38:17 -0400 Subject: [PATCH 060/205] Reject trailing empty path piece in URIs --- src/allmydata/web/directory.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/allmydata/web/directory.py b/src/allmydata/web/directory.py index fced8c68e..b1618a3f5 100644 --- a/src/allmydata/web/directory.py +++ b/src/allmydata/web/directory.py @@ -102,8 +102,9 @@ class DirectoryNodeHandler(ReplaceMeMixin, Resource, object): # trying to replicate what I have observed as Nevow behavior # for these nodes, which is that a URI like # "/uri/URI%3ADIR2%3Aj...vq/" (that is, with a trailing slash - # or no further children) renders "this" page - if not name: + # or no further children) renders "this" page. We also need + # to reject "/uri/URI:DIR2:..//", so we look at postpath. + if not name and req.postpath != ['']: return self # Rejecting URIs that contain empty path pieces (for example: From fccc3316711523b021f46f0b2b932da9080f5ff4 Mon Sep 17 00:00:00 2001 From: Sajith Sasidharan Date: Fri, 29 May 2020 21:38:35 -0400 Subject: [PATCH 061/205] Make "Refresh" link work This seems to be a subtle difference from nevow: with `href="."`, rendered link target will be `/uri/`, so clicking "Refresh" will result in an error message like so: "GET /uri requires uri=". With `href=""`, the rendered link target will be `/uri/URI:...`, which is what we need. --- src/allmydata/web/directory.xhtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/allmydata/web/directory.xhtml b/src/allmydata/web/directory.xhtml index 88058efa4..eeb04692d 100644 --- a/src/allmydata/web/directory.xhtml +++ b/src/allmydata/web/directory.xhtml @@ -31,7 +31,7 @@