diff --git a/README.md b/README.md
index c86fc01a..b1293d0a 100644
--- a/README.md
+++ b/README.md
@@ -72,7 +72,7 @@ you may also want these, especially on servers:
* ☑ symlink/discard existing files (content-matching)
* download
* ☑ single files in browser
- * ✖ folders as zip files
+ * ☑ folders as zip files *(not in release yet)*
* ☑ FUSE client (read-only)
* browser
* ☑ tree-view
diff --git a/copyparty/__main__.py b/copyparty/__main__.py
index 90995506..89d0a8c2 100644
--- a/copyparty/__main__.py
+++ b/copyparty/__main__.py
@@ -261,6 +261,7 @@ def main():
ap.add_argument("-nw", action="store_true", help="disable writes (benchmark)")
ap.add_argument("-nih", action="store_true", help="no info hostname")
ap.add_argument("-nid", action="store_true", help="no info disk-usage")
+ ap.add_argument("--no-zip", action="store_true", help="disable download as zip/tar")
ap.add_argument("--no-sendfile", action="store_true", help="disable sendfile (for debugging)")
ap.add_argument("--no-scandir", action="store_true", help="disable scandir (for debugging)")
ap.add_argument("--urlform", metavar="MODE", type=str, default="print,get", help="how to handle url-forms")
diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py
index c1139772..05696d67 100644
--- a/copyparty/authsrv.py
+++ b/copyparty/authsrv.py
@@ -4,6 +4,7 @@ from __future__ import print_function, unicode_literals
import re
import os
import sys
+import stat
import threading
from .__init__ import PY2, WINDOWS
@@ -127,6 +128,64 @@ class VFS(object):
return [abspath, real, virt_vis]
+ def walk(self, rel, rem, uname, dots, scandir, lstat=False):
+ """
+ recursively yields from ./rem;
+ rel is a unix-style user-defined vpath (not vfs-related)
+ """
+
+ fsroot, vfs_ls, vfs_virt = self.ls(rem, uname, scandir, lstat)
+ rfiles = [x for x in vfs_ls if not stat.S_ISDIR(x[1].st_mode)]
+ rdirs = [x for x in vfs_ls if stat.S_ISDIR(x[1].st_mode)]
+
+ rfiles.sort()
+ rdirs.sort()
+
+ yield rel, fsroot, rfiles, rdirs, vfs_virt
+
+ for rdir, _ in rdirs:
+ if not dots and rdir.startswith("."):
+ continue
+
+ wrel = (rel + "/" + rdir).lstrip("/")
+ wrem = (rem + "/" + rdir).lstrip("/")
+ for x in self.walk(wrel, wrem, uname, scandir, lstat):
+ yield x
+
+ for n, vfs in sorted(vfs_virt.items()):
+ if not dots and n.startswith("."):
+ continue
+
+ wrel = (rel + "/" + n).lstrip("/")
+ for x in vfs.walk(wrel, "", uname, scandir, lstat):
+ yield x
+
+ def zipgen(self, rems, uname, dots, scandir):
+ vtops = [["", [self, ""]]]
+ if rems:
+ # list of subfolders to zip was provided,
+ # add all the ones uname is allowed to access
+ vtops = []
+ for rem in rems:
+ try:
+ vn = self.get(rem, uname, True, False)
+ vtops.append([rem, vn])
+ except:
+ pass
+
+ for rel, (vn, rem) in vtops:
+ for vpath, apath, files, _, _ in vn.walk(rel, rem, uname, dots, scandir):
+ # print(repr([vpath, apath, [x[0] for x in files]]))
+ files = [x for x in files if dots or not x[0].startswith(".")]
+ fnames = [n[0] for n in files]
+ vpaths = [vpath + "/" + n for n in fnames] if vpath else fnames
+ apaths = [os.path.join(apath, n) for n in fnames]
+ for f in [
+ {"vp": vp, "ap": ap, "st": n[1]}
+ for vp, ap, n in zip(vpaths, apaths, files)
+ ]:
+ yield f
+
def user_tree(self, uname, readable=False, writable=False):
ret = []
opt1 = readable and (uname in self.uread or "*" in self.uread)
diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py
index 3598f914..4393aa61 100644
--- a/copyparty/httpcli.py
+++ b/copyparty/httpcli.py
@@ -7,6 +7,7 @@ import gzip
import time
import copy
import json
+import string
import socket
import ctypes
from datetime import datetime
@@ -14,6 +15,8 @@ import calendar
from .__init__ import E, PY2, WINDOWS
from .util import * # noqa # pylint: disable=unused-wildcard-import
+from .szip import StreamZip
+from .star import StreamTar
if not PY2:
unicode = str
@@ -1044,6 +1047,63 @@ class HttpCli(object):
self.log("{}, {}".format(logmsg, spd))
return ret
+ def tx_zip(self, vn, rems, dots):
+ if self.args.no_zip:
+ raise Pebkac(400, "not enabled")
+
+ logmsg = "{:4} {} ".format("", self.req)
+ self.keepalive = False
+
+ fmt = "zip"
+ if fmt == "tar":
+ mime = "application/x-tar"
+ else:
+ mime = "application/zip"
+
+ if rems and rems[0]:
+ fn = rems[0]
+ else:
+ fn = self.vpath.rstrip("/").split("/")[-1]
+
+ if not fn:
+ fn = self.headers.get("host", "hey")
+
+ afn = "".join(
+ [x if x in (string.ascii_letters + string.digits) else "_" for x in fn]
+ )
+
+ ufn = "".join(
+ [
+ x
+ if x in (string.ascii_letters + string.digits)
+ else "%{:02x}".format(ord(x))
+ for x in fn
+ ]
+ )
+
+ cdis = 'attachment; filename="{}.{}", filename*=UTF-8''{}.{}"
+ cdis = cdis.format(afn, fmt, ufn, fmt)
+ self.send_headers(None, mime=mime, headers={"Content-Disposition": cdis})
+
+ fgen = vn.zipgen(rems, self.uname, dots, not self.args.no_scandir)
+ # for f in fgen: print(repr({k: f[k] for k in ["vp", "ap"]}))
+ bgen = StreamZip(fgen, False, False)
+ bsent = 0
+ for buf in bgen.gen():
+ if not buf:
+ break
+
+ try:
+ self.s.sendall(buf)
+ bsent += len(buf)
+ except:
+ logmsg += " \033[31m" + unicode(bsent) + "\033[0m"
+ break
+
+ spd = self._spd(bsent)
+ self.log("{}, {}".format(logmsg, spd))
+ return True
+
def tx_md(self, fs_path):
logmsg = "{:4} {} ".format("", self.req)
@@ -1190,6 +1250,9 @@ class HttpCli(object):
return self.tx_file(abspath)
+ if "zip" in self.uparam:
+ return self.tx_zip(vn, None, False)
+
fsroot, vfs_ls, vfs_virt = vn.ls(rem, self.uname, not self.args.no_scandir)
stats = {k: v for k, v in vfs_ls}
vfs_ls = [x[0] for x in vfs_ls]
@@ -1250,8 +1313,11 @@ class HttpCli(object):
is_dir = stat.S_ISDIR(inf.st_mode)
if is_dir:
- margin = "DIR"
href += "/"
+ if self.args.no_zip:
+ margin = "DIR"
+ else:
+ margin = 'zip'.format(html_escape(href))
elif fn in hist:
margin = '#{}'.format(
base, html_escape(hist[fn][2], quote=True), hist[fn][0]
diff --git a/copyparty/star.py b/copyparty/star.py
index f7f6440b..3c26376a 100644
--- a/copyparty/star.py
+++ b/copyparty/star.py
@@ -1,8 +1,7 @@
-import os
import tarfile
import threading
-from .util import Queue
+from .util import Queue, fsenc
class QFile(object):
@@ -46,11 +45,11 @@ class StreamTar(object):
def _gen(self):
for f in self.fgen:
- src = f["a"]
- name = f["n"]
- inf = tarfile.TarInfo(name=name)
+ name = f["vp"]
+ src = f["ap"]
+ fsi = f["st"]
- fsi = os.stat(src)
+ inf = tarfile.TarInfo(name=name)
inf.mode = fsi.st_mode
inf.size = fsi.st_size
inf.mtime = fsi.st_mtime
@@ -58,7 +57,7 @@ class StreamTar(object):
inf.gid = 0
self.ci += inf.size
- with open(src, "rb") as f:
+ with open(fsenc(src), "rb", 512 * 1024) as f:
self.tar.addfile(inf, f)
self.tar.close()
diff --git a/copyparty/szip.py b/copyparty/szip.py
index 9c7a5e05..d9e14344 100644
--- a/copyparty/szip.py
+++ b/copyparty/szip.py
@@ -174,8 +174,7 @@ def gen_ecdr64_loc(ecdr64_pos):
class StreamZip(object):
- def __init__(self, top, fgen, utf8, pre_crc):
- self.top = top
+ def __init__(self, fgen, utf8, pre_crc):
self.fgen = fgen
self.utf8 = utf8
self.pre_crc = pre_crc
@@ -189,10 +188,10 @@ class StreamZip(object):
def gen(self):
for f in self.fgen:
- src = f["a"]
- name = f["n"]
+ name = f["vp"]
+ src = f["ap"]
+ st = f["st"]
- st = os.stat(fsenc(src))
sz = st.st_size
ts = st.st_mtime + 1
@@ -201,9 +200,9 @@ class StreamZip(object):
yield self._ct(buf)
crc = 0
- with open(src, "rb") as f:
+ with open(fsenc(src), "rb", 512 * 1024) as f:
while True:
- buf = f.read(32768)
+ buf = f.read(64 * 1024)
if not buf:
break