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)