diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 695c5213..88a83105 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -12,7 +12,6 @@ import random import re import socket import stat -import string import sys import threading # typechk import time @@ -31,7 +30,7 @@ try: except: pass -from .__init__ import ANYWIN, PY2, RES, TYPE_CHECKING, EnvParams, unicode +from .__init__ import ANYWIN, RES, TYPE_CHECKING, EnvParams, unicode from .__version__ import S_VERSION from .authsrv import LEELOO_DALLAS, VFS # typechk from .bos import bos @@ -66,6 +65,7 @@ from .util import ( exclude_dotfiles, formatdate, fsenc, + gen_content_disposition, gen_filekey, gen_filekey_dbg, gencookie, @@ -4013,6 +4013,13 @@ class HttpCli(object): if not editions: return self.tx_404() + # + # force download + + if "dl" in self.ouparam: + cdis = gen_content_disposition(os.path.basename(req_path)) + self.out_headers["Content-Disposition"] = cdis + # # if-modified @@ -4181,6 +4188,13 @@ class HttpCli(object): if not editions: return self.tx_404() + # + # force download + + if "dl" in self.ouparam: + cdis = gen_content_disposition(os.path.basename(req_path)) + self.out_headers["Content-Disposition"] = cdis + # # if-modified @@ -4729,24 +4743,7 @@ class HttpCli(object): if maxn < nf: raise Pebkac(400, t) - safe = (string.ascii_letters + string.digits).replace("%", "") - afn = "".join([x if x in safe.replace('"', "") else "_" for x in fn]) - bascii = unicode(safe).encode("utf-8") - zb = fn.encode("utf-8", "xmlcharrefreplace") - if not PY2: - zbl = [ - chr(x).encode("utf-8") - if x in bascii - else "%{:02x}".format(x).encode("ascii") - for x in zb - ] - else: - zbl = [unicode(x) if x in bascii else "%{:02x}".format(ord(x)) for x in zb] - - ufn = b"".join(zbl).decode("ascii") - - cdis = "attachment; filename=\"{}.{}\"; filename*=UTF-8''{}.{}" - cdis = cdis.format(afn, ext, ufn, ext) + cdis = gen_content_disposition("%s.%s" % (fn, ext)) self.log(repr(cdis)) self.send_headers(None, mime=mime, headers={"Content-Disposition": cdis}) diff --git a/copyparty/util.py b/copyparty/util.py index 97351f3c..76b68877 100644 --- a/copyparty/util.py +++ b/copyparty/util.py @@ -52,6 +52,7 @@ from .__init__ import ( VT100, WINDOWS, EnvParams, + unicode, ) from .__version__ import S_BUILD_DT, S_VERSION from .stolen import surrogateescape @@ -115,6 +116,10 @@ IP6ALL = "0:0:0:0:0:0:0:0" IP6_LL = ("fe8", "fe9", "fea", "feb") IP64_LL = ("fe8", "fe9", "fea", "feb", "169.254") +UC_CDISP = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._" +BC_CDISP = UC_CDISP.encode("ascii") +UC_CDISP_SET = set(UC_CDISP) +BC_CDISP_SET = set(BC_CDISP) try: import fcntl @@ -2073,6 +2078,29 @@ def gencookie( ) +def gen_content_disposition(fn: str) -> str: + safe = UC_CDISP_SET + bsafe = BC_CDISP_SET + fn = fn.replace("/", "_").replace("\\", "_") + zb = fn.encode("utf-8", "xmlcharrefreplace") + if not PY2: + zbl = [ + chr(x).encode("utf-8") + if x in bsafe + else "%{:02X}".format(x).encode("ascii") + for x in zb + ] + else: + zbl = [unicode(x) if x in bsafe else "%{:02X}".format(ord(x)) for x in zb] + + ufn = b"".join(zbl).decode("ascii") + afn = "".join([x if x in safe else "_" for x in fn]).lstrip(".") + while ".." in afn: + afn = afn.replace("..", ".") + + return "attachment; filename=\"%s\"; filename*=UTF-8''%s" % (afn, ufn) + + def humansize(sz: float, terse: bool = False) -> str: for unit in HUMANSIZE_UNITS: if sz < 1024: diff --git a/docs/devnotes.md b/docs/devnotes.md index 04c37764..5100471a 100644 --- a/docs/devnotes.md +++ b/docs/devnotes.md @@ -160,6 +160,7 @@ authenticate using header `Cookie: cppwd=foo` or url param `&pw=foo` | method | params | result | |--|--|--| +| GET | `?dl` | download file (don't show in-browser) | | GET | `?ls` | list files/folders at URL as JSON | | GET | `?ls&dots` | list files/folders at URL as JSON, including dotfiles | | GET | `?ls=t` | list files/folders at URL as plaintext |