From de29747dd11967793a42c8a01eea56ca91d74a37 Mon Sep 17 00:00:00 2001 From: ed Date: Tue, 18 Jun 2019 23:29:12 +0000 Subject: [PATCH] add http 206 partial content --- copyparty/httpcli.py | 88 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 76 insertions(+), 12 deletions(-) diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 122efb4d..aa4ddb2c 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -129,7 +129,7 @@ class HttpCli(object): response.extend(headers) response_str = "\r\n".join(response).encode("utf-8") try: - self.s.send(response_str + b"\r\n\r\n" + body) + self.s.sendall(response_str + b"\r\n\r\n" + body) except: raise Pebkac("client disconnected before http response") @@ -140,7 +140,17 @@ class HttpCli(object): self.reply(b"
" + body.encode("utf-8"), *list(args), **kwargs)
 
     def handle_get(self):
-        self.log("{:4} {}".format(self.mode, self.req))
+        logmsg = "{:4} {}".format(self.mode, self.req)
+
+        if "range" in self.headers:
+            try:
+                rval = self.headers["range"].split("=", 1)[1]
+            except:
+                rval += self.headers["range"]
+
+            logmsg += " [\033[36m" + rval + "\033[0m]"
+
+        self.log(logmsg)
 
         # "embedded" resources
         if self.vpath.startswith(".cpr"):
@@ -183,7 +193,7 @@ class HttpCli(object):
 
         try:
             if self.headers["expect"].lower() == "100-continue":
-                self.s.send(b"HTTP/1.1 100 Continue\r\n\r\n")
+                self.s.sendall(b"HTTP/1.1 100 Continue\r\n\r\n")
         except KeyError:
             pass
 
@@ -317,6 +327,13 @@ class HttpCli(object):
         file_lastmod = file_dt.strftime("%a, %b %d %Y %H:%M:%S GMT")
 
         do_send = True
+        status = "200 OK"
+        extra_headers = []
+        logmsg = "{:4} {} {}".format("", self.req, status)
+
+        #
+        # if-modified
+
         if "if-modified-since" in self.headers:
             cli_lastmod = self.headers["if-modified-since"]
             try:
@@ -327,38 +344,85 @@ class HttpCli(object):
                 self.log("bad lastmod format: {}".format(cli_lastmod))
                 do_send = file_lastmod != cli_lastmod
 
-        status = "200 OK"
         if not do_send:
             status = "304 Not Modified"
 
+        #
+        # partial
+
+        file_sz = os.path.getsize(fsenc(path))
+        lower = 0
+        upper = file_sz
+
+        if do_send and "range" in self.headers:
+            try:
+                hrange = self.headers["range"]
+                a, b = hrange.split("=", 1)[1].split("-")
+
+                if a.strip():
+                    lower = int(a.strip())
+                else:
+                    lower = 0
+
+                if b.strip():
+                    upper = int(b.strip()) + 1
+                else:
+                    upper = file_sz
+
+                if lower < 0 or lower >= file_sz or upper < 0 or upper > file_sz:
+                    raise Pebkac("na")
+
+            except:
+                self.loud_reply("invalid range requested: " + hrange)
+
+            status = "206 Partial Content"
+            extra_headers.append(
+                "Content-Range: bytes {}-{}/{}".format(lower, upper - 1, file_sz)
+            )
+
+            logmsg += " [\033[36m" + str(lower) + "-" + str(upper) + "\033[0m]"
+
+        #
+        # send reply
+
         mime = mimetypes.guess_type(path)[0] or "application/octet-stream"
 
         headers = [
             "HTTP/1.1 " + status,
             "Connection: Keep-Alive",
             "Content-Type: " + mime,
-            "Content-Length: " + str(os.path.getsize(fsenc(path))),
+            "Content-Length: " + str(upper - lower),
+            "Accept-Ranges: bytes",
             "Last-Modified: " + file_lastmod,
         ]
-
+        headers.extend(extra_headers)
         headers = "\r\n".join(headers).encode("utf-8") + b"\r\n\r\n"
-        self.s.send(headers)
-
-        logmsg = "{:4} {} {}".format("", self.req, status)
+        self.s.sendall(headers)
 
         if self.mode == "HEAD" or not do_send:
             self.log(logmsg)
             return True
 
-        with open(fsenc(path), "rb") as f:
-            while True:
+        # 512 kB is optimal for huge files, use 64k
+        with open(fsenc(path), "rb", 64 * 1024) as f:
+            remains = upper - lower
+            f.seek(lower)
+            while remains > 0:
+                # time.sleep(0.01)
                 buf = f.read(4096)
                 if not buf:
                     break
 
+                if remains < len(buf):
+                    buf = buf[:remains]
+
+                remains -= len(buf)
+
                 try:
-                    self.s.send(buf)
+                    self.s.sendall(buf)
                 except:
+                    logmsg += " \033[31m" + str(upper - remains) + "\033[0m"
+                    self.log(logmsg)
                     return False
 
         self.log(logmsg)