mirror of
https://github.com/9001/copyparty.git
synced 2025-08-18 01:22:13 -06:00
ftpd: add indexing, delete, windows support
This commit is contained in:
parent
b5f2fe2f0a
commit
c1a7f9edbe
|
@ -626,11 +626,12 @@ using arguments or config files, or a mix of both:
|
|||
|
||||
## ftp-server
|
||||
|
||||
an FTP server can be started using `--ftp 2121` (or any other port)
|
||||
an FTP server can be started using `--ftp 3921` (or any other port)
|
||||
|
||||
* based on [pyftpdlib](https://github.com/giampaolo/pyftpdlib)
|
||||
* needs a dedicated port (cannot share with the HTTP/HTTPS API)
|
||||
* runs in active mode by default, you probably want `--ftp-r`
|
||||
* uploads are not resumable
|
||||
|
||||
|
||||
## file indexing
|
||||
|
|
|
@ -446,8 +446,8 @@ def run_argparse(argv, formatter):
|
|||
ap2.add_argument("--ssl-log", metavar="PATH", type=u, help="log master secrets")
|
||||
|
||||
ap2 = ap.add_argument_group('FTP options')
|
||||
ap2.add_argument("--ftp", metavar="PORT", type=int, help="enable FTP server on PORT")
|
||||
ap2.add_argument("--ftp-debug", action="store_true", help="enable debug logging")
|
||||
ap2.add_argument("--ftp", metavar="PORT", type=int, help="enable FTP server on PORT, for example 3921")
|
||||
ap2.add_argument("--ftp-dbg", action="store_true", help="enable debug logging")
|
||||
ap2.add_argument("--ftp-nat", metavar="ADDR", type=u, help="the NAT address to use for passive connections")
|
||||
ap2.add_argument("--ftp-r", metavar="P-P", type=u, help="the range of TCP ports to use for passive connections, for example 12000-13000")
|
||||
|
||||
|
|
|
@ -394,6 +394,13 @@ class VFS(object):
|
|||
if ok:
|
||||
virt_vis[name] = vn2
|
||||
|
||||
if ".hist" in abspath:
|
||||
p = abspath.replace("\\", "/") if WINDOWS else abspath
|
||||
if p.endswith("/.hist"):
|
||||
real = [x for x in real if not x[0].startswith("up2k.")]
|
||||
elif "/.hist/th/" in p:
|
||||
real = [x for x in real if not x[0].endswith("dir.txt")]
|
||||
|
||||
return [abspath, real, virt_vis]
|
||||
|
||||
def walk(self, rel, rem, seen, uname, permsets, dots, scandir, lstat):
|
||||
|
@ -444,10 +451,6 @@ class VFS(object):
|
|||
if flt:
|
||||
flt = {k: True for k in flt}
|
||||
|
||||
f1 = "{0}.hist{0}up2k.".format(os.sep)
|
||||
f2a = os.sep + "dir.txt"
|
||||
f2b = "{0}.hist{0}".format(os.sep)
|
||||
|
||||
# if multiselect: add all items to archive root
|
||||
# if single folder: the folder itself is the top-level item
|
||||
folder = "" if flt else (vrem.split("/")[-1] or "top")
|
||||
|
@ -483,13 +486,6 @@ class VFS(object):
|
|||
for x in rm:
|
||||
del vd[x]
|
||||
|
||||
# up2k filetring based on actual abspath
|
||||
files = [
|
||||
x
|
||||
for x in files
|
||||
if f1 not in x[1] and (not x[1].endswith(f2a) or f2b not in x[1])
|
||||
]
|
||||
|
||||
for f in [{"vp": v, "ap": a, "st": n[1]} for v, a, n in files]:
|
||||
yield f
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ from __future__ import print_function, unicode_literals
|
|||
|
||||
import os
|
||||
import stat
|
||||
import time
|
||||
import logging
|
||||
import threading
|
||||
from typing import TYPE_CHECKING
|
||||
|
@ -55,7 +56,7 @@ class FtpAuth(DummyAuthorizer):
|
|||
return "elradfmwMT"
|
||||
|
||||
def get_msg_login(self, username):
|
||||
return "sup"
|
||||
return "sup {}".format(username)
|
||||
|
||||
def get_msg_quit(self, username):
|
||||
return "cya"
|
||||
|
@ -64,10 +65,10 @@ class FtpAuth(DummyAuthorizer):
|
|||
class FtpFs(AbstractedFS):
|
||||
def __init__(self, root, cmd_channel):
|
||||
self.h = self.cmd_channel = cmd_channel # type: FTPHandler
|
||||
self.asrv = cmd_channel.asrv # type: AuthSrv
|
||||
self.hub = cmd_channel.hub # type: SvcHub
|
||||
self.args = cmd_channel.args
|
||||
|
||||
self.uname = self.asrv.iacct.get(cmd_channel.password, "*")
|
||||
self.uname = self.hub.asrv.iacct.get(cmd_channel.password, "*")
|
||||
|
||||
self.cwd = "/" # pyftpdlib convention of leading slash
|
||||
self.root = "/var/lib/empty"
|
||||
|
@ -76,8 +77,8 @@ class FtpFs(AbstractedFS):
|
|||
|
||||
def v2a(self, vpath, r=False, w=False, m=False, d=False):
|
||||
try:
|
||||
vpath = vpath.lstrip("/")
|
||||
vfs, rem = self.asrv.vfs.get(vpath, self.uname, r, w, m, d)
|
||||
vpath = vpath.replace("\\", "/").lstrip("/")
|
||||
vfs, rem = self.hub.asrv.vfs.get(vpath, self.uname, r, w, m, d)
|
||||
return os.path.join(vfs.realpath, rem)
|
||||
except Pebkac as ex:
|
||||
raise FilesystemError(str(ex))
|
||||
|
@ -94,18 +95,25 @@ class FtpFs(AbstractedFS):
|
|||
return fspath
|
||||
|
||||
def validpath(self, path):
|
||||
# other funcs handle permission checking implicitly
|
||||
if "/.hist/" in path:
|
||||
if "/up2k." in path or path.endswith("/dir.txt"):
|
||||
raise FilesystemError("access to this file is forbidden")
|
||||
|
||||
return True
|
||||
|
||||
def open(self, filename, mode):
|
||||
ap = self.rv2a(filename, "r" in mode, "w" in mode)
|
||||
if "w" in mode and bos.path.exists(ap):
|
||||
r = "r" in mode
|
||||
w = "w" in mode or "a" in mode or "+" in mode
|
||||
|
||||
ap = self.rv2a(filename, r, w)
|
||||
if w and bos.path.exists(ap):
|
||||
raise FilesystemError("cannot open existing file for writing")
|
||||
|
||||
self.validpath(ap)
|
||||
return open(fsenc(ap), mode)
|
||||
|
||||
def chdir(self, path):
|
||||
self.cwd = os.path.join(self.cwd, path)
|
||||
self.cwd = join(self.cwd, path)
|
||||
|
||||
def mkdir(self, path):
|
||||
ap = self.rv2a(path, w=True)
|
||||
|
@ -113,8 +121,8 @@ class FtpFs(AbstractedFS):
|
|||
|
||||
def listdir(self, path):
|
||||
try:
|
||||
vpath = os.path.join(self.cwd, path).lstrip("/")
|
||||
vfs, rem = self.asrv.vfs.get(vpath, self.uname, True, False)
|
||||
vpath = join(self.cwd, path).lstrip("/")
|
||||
vfs, rem = self.hub.asrv.vfs.get(vpath, self.uname, True, False)
|
||||
|
||||
fsroot, vfs_ls, vfs_virt = vfs.ls(
|
||||
rem, self.uname, not self.args.no_scandir, [[True], [False, True]]
|
||||
|
@ -136,8 +144,18 @@ class FtpFs(AbstractedFS):
|
|||
bos.rmdir(ap)
|
||||
|
||||
def remove(self, path):
|
||||
ap = self.rv2a(path, d=True)
|
||||
bos.unlink(ap)
|
||||
if self.args.no_del:
|
||||
raise Pebkac(403, "the delete feature is disabled in server config")
|
||||
|
||||
vp = join(self.cwd, path).lstrip("/")
|
||||
x = self.hub.broker.put(
|
||||
True, "up2k.handle_rm", self.uname, self.h.remote_ip, [vp]
|
||||
)
|
||||
|
||||
try:
|
||||
x.get()
|
||||
except Exception as ex:
|
||||
raise FilesystemError(str(ex))
|
||||
|
||||
def rename(self, src, dst):
|
||||
raise NotImplementedError()
|
||||
|
@ -186,8 +204,7 @@ class FtpFs(AbstractedFS):
|
|||
return bos.path.getmtime(ap)
|
||||
|
||||
def realpath(self, path):
|
||||
ap = self.rv2a(path)
|
||||
return bos.path.isdir(ap)
|
||||
return path
|
||||
|
||||
def lexists(self, path):
|
||||
ap = self.rv2a(path)
|
||||
|
@ -200,15 +217,56 @@ class FtpFs(AbstractedFS):
|
|||
return "root"
|
||||
|
||||
|
||||
class FtpHandler(FTPHandler):
|
||||
abstracted_fs = FtpFs
|
||||
|
||||
def __init__(self, conn, server, ioloop=None):
|
||||
super(FtpHandler, self).__init__(conn, server, ioloop)
|
||||
|
||||
# abspath->vpath mapping to resolve log_transfer paths
|
||||
self.vfs_map = {}
|
||||
|
||||
def ftp_STOR(self, file, mode="w"):
|
||||
vp = join(self.fs.cwd, file).lstrip("/")
|
||||
ap = self.fs.v2a(vp)
|
||||
self.vfs_map[ap] = vp
|
||||
# print("ftp_STOR: {} {} => {}".format(vp, mode, ap))
|
||||
ret = FTPHandler.ftp_STOR(self, file, mode)
|
||||
# print("ftp_STOR: {} {} OK".format(vp, mode))
|
||||
return ret
|
||||
|
||||
def log_transfer(self, cmd, filename, receive, completed, elapsed, bytes):
|
||||
ap = filename.decode("utf-8", "replace")
|
||||
vp = self.vfs_map.pop(ap, None)
|
||||
# print("xfer_end: {} => {}".format(ap, vp))
|
||||
if vp:
|
||||
vp, fn = os.path.split(vp)
|
||||
vfs, rem = self.hub.asrv.vfs.get(vp, self.username, False, True)
|
||||
vfs, rem = vfs.get_dbv(rem)
|
||||
self.hub.broker.put(
|
||||
False,
|
||||
"up2k.hash_file",
|
||||
vfs.realpath,
|
||||
vfs.flags,
|
||||
rem,
|
||||
fn,
|
||||
self.remote_ip,
|
||||
time.time(),
|
||||
)
|
||||
|
||||
return FTPHandler.log_transfer(
|
||||
self, cmd, filename, receive, completed, elapsed, bytes
|
||||
)
|
||||
|
||||
|
||||
class Ftpd(object):
|
||||
def __init__(self, hub):
|
||||
self.hub = hub
|
||||
self.args = hub.args
|
||||
|
||||
h = FTPHandler
|
||||
h.asrv = hub.asrv
|
||||
h = FtpHandler
|
||||
h.hub = hub
|
||||
h.args = hub.args
|
||||
h.abstracted_fs = FtpFs
|
||||
h.authorizer = FtpAuth()
|
||||
h.authorizer.hub = hub
|
||||
|
||||
|
@ -219,7 +277,7 @@ class Ftpd(object):
|
|||
if self.args.ftp_nat:
|
||||
h.masquerade_address = self.args.ftp_nat
|
||||
|
||||
if self.args.ftp_debug:
|
||||
if self.args.ftp_dbg:
|
||||
config_logging(level=logging.DEBUG)
|
||||
|
||||
ioloop = IOLoop()
|
||||
|
@ -229,3 +287,8 @@ class Ftpd(object):
|
|||
t = threading.Thread(target=ioloop.loop)
|
||||
t.daemon = True
|
||||
t.start()
|
||||
|
||||
|
||||
def join(p1, p2):
|
||||
w = os.path.join(p1, p2.replace("\\", "/"))
|
||||
return os.path.normpath(w).replace("\\", "/")
|
||||
|
|
|
@ -2234,10 +2234,6 @@ class HttpCli(object):
|
|||
if not self.args.ed or "dots" not in self.uparam:
|
||||
vfs_ls = exclude_dotfiles(vfs_ls)
|
||||
|
||||
hidden = []
|
||||
if rem == ".hist":
|
||||
hidden = ["up2k."]
|
||||
|
||||
icur = None
|
||||
if "e2t" in vn.flags:
|
||||
idx = self.conn.get_u2idx()
|
||||
|
@ -2256,8 +2252,6 @@ class HttpCli(object):
|
|||
|
||||
if fn in vfs_virt:
|
||||
fspath = vfs_virt[fn].realpath
|
||||
elif hidden and any(fn.startswith(x) for x in hidden):
|
||||
continue
|
||||
else:
|
||||
fspath = fsroot + "/" + fn
|
||||
|
||||
|
|
Loading…
Reference in a new issue