add landing page with mounting instructions

This commit is contained in:
ed 2022-11-26 19:47:27 +00:00
parent e53531a9fb
commit f0e78a6826
22 changed files with 439 additions and 18 deletions

View file

@ -847,6 +847,7 @@ def run_argparse(
ap2.add_argument("--no-scandir", action="store_true", help="disable scandir; instead using listdir + stat on each file")
ap2.add_argument("--no-fastboot", action="store_true", help="wait for up2k indexing before starting the httpd")
ap2.add_argument("--no-htp", action="store_true", help="disable httpserver threadpool, create threads as-needed instead")
ap2.add_argument("--rclone-mdns", action="store_true", help="use mdns-domain instead of server-ip on /?hc")
ap2.add_argument("--stackmon", metavar="P,S", type=u, help="write stacktrace to Path every S second, for example --stackmon=\033[32m./st/%%Y-%%m/%%d/%%H%%M.xz,60")
ap2.add_argument("--log-thrs", metavar="SEC", type=float, help="list active threads every SEC")
ap2.add_argument("--log-fk", metavar="REGEX", type=u, default="", help="log filekey params for files where path matches REGEX; [\033[32m.\033[0m] (a single dot) = all files")

View file

@ -113,6 +113,10 @@ class BrokerMp(object):
for p in self.procs:
p.q_pend.put((0, dest, [args[0], len(self.procs)]))
elif dest == "set_netdevs":
for p in self.procs:
p.q_pend.put((0, dest, list(args)))
elif dest == "cb_httpsrv_up":
self.hub.cb_httpsrv_up()

View file

@ -97,6 +97,9 @@ class MpWorker(BrokerCli):
elif dest == "listen":
self.httpsrv.listen(args[0], args[1])
elif dest == "set_netdevs":
self.httpsrv.set_netdevs(args[0])
elif dest == "retq":
# response from previous ipc call
with self.retpend_mutex:

View file

@ -61,6 +61,10 @@ class BrokerThr(BrokerCli):
self.httpsrv.listen(args[0], 1)
return
if dest == "set_netdevs":
self.httpsrv.set_netdevs(args[0])
return
# new ipc invoking managed service in hub
obj = self.hub
for node in dest.split("."):

View file

@ -691,6 +691,9 @@ class HttpCli(object):
if "reset" in self.uparam:
return self.set_cfg_reset()
if "hc" in self.uparam:
return self.tx_svcs()
if "h" in self.uparam:
return self.tx_mounts()
@ -2562,6 +2565,32 @@ class HttpCli(object):
return True
def tx_svcs(self) -> bool:
aname = re.sub("[^0-9a-zA-Z]+", "", self.args.name) or "a"
ep = self.headers["host"]
host = ep.split(":")[0]
hport = ep[ep.find(":") :] if ":" in ep else ""
rip = (
host
if self.args.rclone_mdns or not self.args.zm
else self.conn.hsrv.nm.map(self.ip) or host
)
html = self.j2s(
"svcs",
args=self.args,
accs=bool(self.asrv.acct),
s="s" if self.is_https else "",
rip=rip,
ep=ep,
vp=(self.uparam["hc"] or "").lstrip("/"),
host=host,
hport=hport,
aname=aname,
pw=self.pw or "pw",
)
self.reply(html.encode("utf-8"))
return True
def tx_mounts(self) -> bool:
suf = self.urlq({}, ["h"])
avol = [x for x in self.wvol if x in self.rvol]

View file

@ -37,6 +37,7 @@ from .util import (
Daemon,
Garda,
Magician,
NetMap,
ipnorm,
min_ex,
shut_socket,
@ -72,6 +73,7 @@ class HttpSrv(object):
nsuf = "-n{}-i{:x}".format(nid, os.getpid()) if nid else ""
self.magician = Magician()
self.nm = NetMap([], {})
self.ssdp: Optional["SSDPr"] = None
self.gpwd = Garda(self.args.ban_pw)
self.g404 = Garda(self.args.ban_404)
@ -102,10 +104,8 @@ class HttpSrv(object):
env = jinja2.Environment()
env.loader = jinja2.FileSystemLoader(os.path.join(self.E.mod, "web"))
self.j2 = {
x: env.get_template(x + ".html")
for x in ["splash", "browser", "browser2", "msg", "md", "mde", "cf"]
}
jn = ["splash", "svcs", "browser", "browser2", "msg", "md", "mde", "cf"]
self.j2 = {x: env.get_template(x + ".html") for x in jn}
zs = os.path.join(self.E.mod, "web", "deps", "prism.js.gz")
self.prism = os.path.exists(zs)
@ -140,6 +140,9 @@ class HttpSrv(object):
except:
pass
def set_netdevs(self, netdevs: dict[str, str]) -> None:
self.nm = NetMap([self.ip], netdevs)
def start_threads(self, n: int) -> None:
self.tp_nthr += n
if self.args.log_htp:

View file

@ -257,7 +257,12 @@ class MDNS(MCast):
self.log("sendto failed: {} ({})".format(srv.ip, ex), "90")
def run(self) -> None:
try:
bound = self.create_servers()
except:
self.log("no server IP matches the mdns config", 1)
bound = []
if not bound:
self.log("failed to announce copyparty services on the network", 3)
return

View file

@ -20,6 +20,10 @@ if not hasattr(socket, "IPPROTO_IPV6"):
setattr(socket, "IPPROTO_IPV6", 41)
class NoIPs(Exception):
pass
class MC_Sck(object):
"""there is one socket for each server ip"""
@ -114,7 +118,7 @@ class MCast(object):
ips = [x for x in ips if ":" not in x or x.startswith("fe80")]
if not ips:
raise Exception("no server IP matches the mdns config")
raise NoIPs()
for ip in ips:
v6 = ":" in ip
@ -302,8 +306,8 @@ class MCast(object):
t = "could not map client {} to known subnet; maybe forwarded from another network?"
self.log(t.format(cip), 3)
self.cscache[cip] = ret
if len(self.cscache) > 9000:
self.cscache = {}
self.cscache[cip] = ret
return ret

View file

@ -99,7 +99,12 @@ class SSDPd(MCast):
self.log_func("SSDP", msg, c)
def run(self) -> None:
try:
bound = self.create_servers()
except:
self.log("no server IP matches the ssdp config", 1)
bound = []
if not bound:
self.log("failed to announce copyparty services on the network", 3)
return

View file

@ -252,6 +252,7 @@ class TcpSrv(object):
self.srv = srvs
self.bound = bound
self.nsrv = len(srvs)
self.hub.broker.say("set_netdevs", self.netdevs)
def shutdown(self) -> None:
self.stopping = True

View file

@ -26,10 +26,10 @@ from collections import Counter
from datetime import datetime
from email.utils import formatdate
from ipaddress import IPv6Address
from ipaddress import IPv4Address, IPv4Network, IPv6Address, IPv6Network
from queue import Queue
from .__init__ import ANYWIN, MACOS, PY2, TYPE_CHECKING, VT100, WINDOWS, unicode
from .__init__ import ANYWIN, MACOS, PY2, TYPE_CHECKING, VT100, WINDOWS
from .__version__ import S_BUILD_DT, S_VERSION
from .stolen import surrogateescape
@ -95,6 +95,8 @@ if TYPE_CHECKING:
from .authsrv import VFS
FAKE_MP = False
try:
import multiprocessing as mp
@ -429,6 +431,52 @@ class HLog(logging.Handler):
self.log_func(record.name[-21:], msg, c)
class NetMap(object):
def __init__(self, ips: list[str], netdevs: dict[str, str]) -> None:
if "::" in ips:
ips = [x for x in ips if x != "::"] + list(
[x.split("/")[0] for x in netdevs if ":" in x]
)
ips.append("0.0.0.0")
if "0.0.0.0" in ips:
ips = [x for x in ips if x != "0.0.0.0"] + list(
[x.split("/")[0] for x in netdevs if ":" not in x]
)
ips = [x for x in ips if x not in ("::1", "127.0.0.1")]
ips = [[x for x in netdevs if x.startswith(y + "/")][0] for y in ips]
self.cache: dict[str, str] = {}
self.b2sip: dict[bytes, str] = {}
self.b2net: dict[bytes, Union[IPv4Network, IPv6Network]] = {}
self.bip: list[bytes] = []
for ip in ips:
v6 = ":" in ip
fam = socket.AF_INET6 if v6 else socket.AF_INET
bip = socket.inet_pton(fam, ip.split("/")[0])
self.bip.append(bip)
self.b2sip[bip] = ip.split('/')[0]
self.b2net[bip] = (IPv6Network if v6 else IPv4Network)(ip, False)
self.bip.sort(reverse=True)
def map(self, ip: str) -> str:
try:
return self.cache[ip]
except:
pass
v6 = ":" in ip
ci = IPv6Address(ip) if v6 else IPv4Address(ip)
bip = next((x for x in self.bip if ci in self.b2net[x]), None)
ret = self.b2sip[bip] if bip else ""
if len(self.cache) > 9000:
self.cache = {}
self.cache[ip] = ret
return ret
class UnrecvEOF(OSError):
pass

1
copyparty/web/a/up2k.py Symbolic link
View file

@ -0,0 +1 @@
../../../bin/up2k.py

View file

@ -0,0 +1 @@
../../../contrib/webdav-cfg.bat

View file

@ -10,6 +10,9 @@ html {
padding: 0 1em 3em 1em;
line-height: 1.3em;
}
#wrap.w {
max-width: 96%;
}
h1 {
border-bottom: 1px solid #ccc;
margin: 2em 0 .4em 0;
@ -25,15 +28,16 @@ a {
text-decoration: none;
border-bottom: 1px solid #8ab;
border-radius: .2em;
padding: .2em .8em;
padding: .2em .6em;
margin: 0 .3em;
}
a+a {
margin-left: .5em;
td a {
margin: 0;
}
.refresh,
.af,
.logout {
float: right;
margin: -.2em 0 0 .5em;
margin: -.2em 0 0 .8em;
}
.logout,
a.r {
@ -64,9 +68,15 @@ table {
.num td:first-child {
text-align: right;
}
.c {
text-align: center;
}
.btns {
margin: 1em 0;
}
.btns>a:first-child {
margin-left: 0;
}
#msg {
margin: 3em 0;
}
@ -83,6 +93,41 @@ blockquote {
border-left: .3em solid rgba(128,128,128,0.5);
border-radius: 0 0 0 .25em;
}
pre, code {
color: #480;
background: #fff;
font-family: 'scp', monospace, monospace;
border: 1px solid rgba(128,128,128,0.3);
border-radius: .2em;
padding: .15em .2em;
}
html.z pre,
html.z code {
color: #9e0;
background: rgba(0,16,0,0.2);
}
.ossel a.r {
box-shadow: 0 .3em 1em #c04;
}
.os {
line-height: 1.5em;
}
.sph {
margin-top: 4em;
}
.sph code {
margin-left: .3em;
}
pre b,
code b {
color: #000;
font-weight: normal;
text-shadow: 0 0 .2em #0f0;
}
html.z pre b,
html.z code b {
color: #fff;
}
html.z {
@ -102,6 +147,9 @@ html.z a.r {
background: #804;
border-color: #c28;
}
html.z .ossel a.r {
box-shadow: 0 .3em 1em #704, 0 .3em 1em #704;
}
html.z input {
color: #fff;
background: #626;

View file

@ -13,7 +13,8 @@
<body>
<div id="wrap">
<a id="a" href="/?h" class="refresh">refresh</a>
<a id="a" href="/?h" class="af">refresh</a>
<a id="v" href="/?hc" class="af">connect</a>
{%- if this.uname == '*' %}
<p id="b">howdy stranger &nbsp; <small>(you're not logged in)</small></p>

View file

@ -24,11 +24,14 @@ var Ls = {
".s1": "kartlegg",
"t1": "handling",
"u2": "tid siden noen sist skrev til serveren$N( opplastning / navneendring / ... )$N$N17d = 17 dager$N1h23 = 1 time 23 minutter$N4m56 = 4 minuter 56 sekunder",
"v1": "koble til",
"v2": "bruk denne serveren som en lokal harddisk$N$NADVARSEL: kommer til å vise passordet ditt!"
},
"eng": {
"d2": "shows the state of all active threads",
"e2": "reload config files (accounts/volumes/volflags),$Nand rescan all e2ds volumes",
"u2": "time since the last server write$N( upload / rename / ... )$N$N17d = 17 days$N1h23 = 1 hour 23 minutes$N4m56 = 4 minutes 56 seconds",
"v2": "use this server as a local HDD$N$NWARNING: this will show your password!",
}
},
d = Ls[sread("lang") || lang];

199
copyparty/web/svcs.html Normal file
View file

@ -0,0 +1,199 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{{ args.doctitle }} @ {{ args.name }}</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=0.8">
{{ html_head }}
<link rel="stylesheet" media="screen" href="/.cpr/splash.css?_={{ ts }}">
<link rel="stylesheet" media="screen" href="/.cpr/ui.css?_={{ ts }}">
</head>
<body>
<div id="wrap" class="w">
<div class="c">
<p class="btns"><a href="/">browse files</a> // <a href="/?h">control panel</a></p>
<p>or choose your OS for cooler alternatives:</p>
<div class="ossel">
<a id="swin" href="#">Windows</a>
<a id="slin" href="#">Linux</a>
<a id="smac" href="#">macOS</a>
</div>
</div>
<p class="sph">
make this server appear on your computer as a regular HDD!<br />
pick your favorite below (sorted by performance, best first) and lets 🎉<br />
<br />
placeholders:
<span class="os win">
{% if accs %}<code><b>{{ pw }}</b></code>=password, {% endif %}<code><b>W:</b></code>=mountpoint
</span>
<span class="os lin mac">
{% if accs %}<code><b>{{ pw }}</b></code>=password, {% endif %}<code><b>mp</b></code>=mountpoint
</span>
</p>
{% if not args.no_dav %}
<h1>WebDAV</h1>
<div class="os win">
<p><em>note: rclone-FTP is a bit faster, so {% if args.ftp or args.ftps %}try that first{% else %}consider enabling FTP in server settings{% endif %}</em></p>
<p>if you can, install <a href="https://winfsp.dev/rel/">winfsp</a>+<a href="https://downloads.rclone.org/rclone-current-windows-amd64.zip">rclone</a> and then paste this in cmd:</p>
<pre>
rclone config create {{ aname }}-dav webdav url=http{{ s }}://{{ rip }}{{ hport }} vendor=other{% if accs %} user=k pass=<b>{{ pw }}</b>{% endif %}
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-dav:{{ vp }} <b>W:</b>
</pre>
{% if s %}
<p><em>note: if you are on LAN (or just dont have valid certificates), add <code>--no-check-certificate</code> to the mount command</em><br />---</p>
{% endif %}
<p>if you want to use the native WebDAV client in windows instead (slow and buggy), first run <a href="/.cpr/a/webdav-cfg.bat">webdav-cfg.bat</a> to remove the 47 MiB filesize limit (also fixes password login), then connect:</p>
<pre>
net use <b>w:</b> http{{ s }}://{{ ep }}/{{ vp }}{% if accs %} k /user:<b>{{ pw }}</b>{% endif %}
</pre>
</div>
<div class="os lin">
<pre>
yum install davfs2
{% if accs %}printf '%s\n' <b>{{ pw }}</b> k | {% endif %}mount -t davfs -ouid=1000 http{{ s }}://{{ ep }}/{{ vp }} <b>mp</b>
</pre>
<p>or you can use rclone instead, which is much slower but doesn't require root:</p>
<pre>
rclone config create {{ aname }}-dav webdav url=http{{ s }}://{{ rip }}{{ hport }} vendor=other{% if accs %} user=k pass=<b>{{ pw }}</b>{% endif %}
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-dav:{{ vp }} <b>mp</b>
</pre>
{% if s %}
<p><em>note: if you are on LAN (or just dont have valid certificates), add <code>--no-check-certificate</code> to the mount command</em><br />---</p>
{% endif %}
<p>or the emergency alternative (gnome/gui-only):</p>
<!-- gnome-bug: ignores vp -->
<pre>
{%- if accs %}
echo <b>{{ pw }}</b> | gio mount dav{{ s }}://k@{{ ep }}/{{ vp }}
{%- else %}
gio mount -a dav{{ s }}://{{ ep }}/{{ vp }}
{%- endif %}
</pre>
</div>
<div class="os mac">
<pre>
osascript -e ' mount volume "http{{ s }}://k:<b>{{ pw }}</b>@{{ ep }}/{{ vp }}" '
</pre>
<p>or you can open up a Finder, press command-K and paste this instead:</p>
<pre>
http{{ s }}://k:<b>{{ pw }}</b>@{{ ep }}/{{ vp }}
</pre>
{% if s %}
<p><em>replace <code>https</code> with <code>http</code> if it doesn't work</em></p>
{% endif %}
</div>
{% endif %}
{% if args.ftp or args.ftps %}
<h1>FTP</h1>
<div class="os win">
<p>if you can, install <a href="https://winfsp.dev/rel/">winfsp</a>+<a href="https://downloads.rclone.org/rclone-current-windows-amd64.zip">rclone</a> and then paste this in cmd:</p>
<pre>
rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp or args.ftps }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls={{ "false" if args.ftp else "true" }}
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-ftp:{{ vp }} <b>W:</b>
</pre>
<p>if you want to use the native FTP client in windows instead (please dont), press <code>win+R</code> and run this command:</p>
<pre>
explorer {{ "ftp" if args.ftp else "ftps" }}://{% if accs %}<b>{{ pw }}</b>:k@{% endif %}{{ host }}:{{ args.ftp or args.ftps }}/{{ vp }}
</pre>
</div>
<div class="os lin">
<pre>
rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp or args.ftps }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls={{ "false" if args.ftp else "true" }}
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-ftp:{{ vp }} <b>mp</b>
</pre>
<p>emergency alternative (gnome/gui-only):</p>
<!-- gnome-bug: ignores vp -->
<pre>
{%- if accs %}
echo <b>{{ pw }}</b> | gio mount ftp{{ "" if args.ftp else "s" }}://k@{{ host }}:{{ args.ftp or args.ftps }}/{{ vp }}
{%- else %}
gio mount -a ftp{{ "" if args.ftp else "s" }}://{{ host }}:{{ args.ftp or args.ftps }}/{{ vp }}
{%- endif %}
</pre>
</div>
<div class="os mac">
<p>note: FTP is read-only on macos; please use WebDAV instead</p>
<pre>
open {{ "ftp" if args.ftp else "ftps" }}://{% if accs %}k:<b>{{ pw }}</b>@{% else %}anonymous:@{% endif %}{{ host }}:{{ args.ftp or args.ftps }}/{{ vp }}
</pre>
</div>
{% endif %}
<h1>partyfuse</h1>
<p>
<a href="/.cpr/a/partyfuse.py">partyfuse.py</a> -- fast, read-only,
<span class="os win">needs <a href="https://winfsp.dev/rel/">winfsp</a></span>
<span class="os lin">doesn't need root</span>
</p>
<pre>
partyfuse.py{% if accs %} -a <b>{{ pw }}</b>{% endif %} http{{ s }}://{{ ep }}/{{ vp }} <b><span class="os win">W:</span><span class="os lin mac">mp</span></b>
</pre>
{% if s %}
<p><em>note: if you are on LAN (or just dont have valid certificates), add <code>-td</code></p>
{% endif %}
<p>
you can use <a href="/.cpr/a/up2k.py">up2k.py</a> to upload (sometimes faster than web-browsers)
</p>
{% if args.smb %}
<h1>SMB / CIFS</h1>
<em><a href="https://github.com/SecureAuthCorp/impacket/issues/1433">bug:</a> max ~300 files in each folder</em>
<div class="os win">
<pre>
net use <b>w:</b> \\{{ host }}\a{% if accs %} k /user:<b>{{ pw }}</b>{% endif %}
</pre>
<!-- rclone fails due to copyparty-smb bugs -->
</div>
<div class="os lin">
<pre>
mount -t cifs -o{% if accs %}user=<b>{{ pw }}</b>,pass=k,{% endif %}vers={{ 1 if args.smb1 else 2 }}.0,port={{ args.smb_port }},uid=1000 //{{ host }}/a/ <b>mp</b>
</pre>
<!-- p>or the emergency alternative (gnome/gui-only):</p nevermind, only works through mdns -->
</div>
<pre class="os mac">
@@@ net use <b>w:</b> \\{{ host }}\a k /user:<b>{{ pw }}</b>
</pre>
{% endif %}
</div>
<a href="#" id="repl">π</a>
<script>
var lang="{{ lang }}",
dfavico="{{ favico }}";
document.documentElement.className=localStorage.theme||"{{ args.theme }}";
</script>
<script src="/.cpr/util.js?_={{ ts }}"></script>
<script src="/.cpr/svcs.js?_={{ ts }}"></script>
</body>
</html>

34
copyparty/web/svcs.js Normal file
View file

@ -0,0 +1,34 @@
var oa = QSA('pre');
for (var a = 0; a < oa.length; a++) {
var html = oa[a].innerHTML,
nd = /^ +/.exec(html)[0].length,
rd = new RegExp('(^|\n) {' + nd + '}', 'g');
oa[a].innerHTML = html.replace(rd, '$1').replace(/[ \r\n]+$/, '');
}
oa = QSA('.ossel a');
for (var a = 0; a < oa.length; a++)
oa[a].onclick = esetos;
function esetos(e) {
ev(e);
setos(e.target.id.slice(1));
}
function setos(os) {
var oa = QSA('.os');
for (var a = 0; a < oa.length; a++)
oa[a].style.display = 'none';
var oa = QSA('.' + os);
for (var a = 0; a < oa.length; a++)
oa[a].style.display = '';
oa = QSA('.ossel a');
for (var a = 0; a < oa.length; a++)
clmod(oa[a], 'r', oa[a].id.slice(1) == os);
}
setos(WINDOWS ? 'win' : LINUX ? 'lin' : MACOS ? 'mac' : '');

View file

@ -17,7 +17,9 @@ var wah = '',
VCHROME = CHROME ? 1 : 0,
FIREFOX = ('netscape' in window) && / rv:/.test(navigator.userAgent),
IPHONE = TOUCH && /iPhone|iPad|iPod/i.test(navigator.userAgent),
WINDOWS = navigator.platform ? navigator.platform == 'Win32' : /Windows/.test(navigator.userAgent);
LINUX = /Linux/.test(navigator.userAgent),
MACOS = /[^a-z]mac ?os/i.test(navigator.userAgent),
WINDOWS = /Windows/.test(navigator.userAgent);
if (!window.WebAssembly || !WebAssembly.Memory)
window.WebAssembly = false;

View file

@ -92,7 +92,11 @@ rm -rf build/pypi
mkdir -p build/pypi
cp -pR setup.py README.md LICENSE copyparty tests bin scripts/strip_hints build/pypi/
cd build/pypi
tar --strip-components=2 -xf ../strip-hints-0.1.10.tar.gz strip-hints-0.1.10/src/strip_hints
f=../strip-hints-0.1.10.tar.gz
[ -e $f ] ||
(url=https://files.pythonhosted.org/packages/9c/d4/312ddce71ee10f7e0ab762afc027e07a918f1c0e1be5b0069db5b0e7542d/strip-hints-0.1.10.tar.gz;
wget -O$f "$url" || curl -L "$url" >$f)
tar --strip-components=2 -xf $f strip-hints-0.1.10/src/strip_hints
python3 -c 'from strip_hints.a import uh; uh("copyparty")'
./setup.py clean2

View file

@ -245,6 +245,21 @@ tmpdir="$(
}
rm -f ../tar
# resolve symlinks
find -type l |
while IFS= read -r f1; do (
cd "${f1%/*}"
f1="./${f1##*/}"
f2="$(readlink "$f1")"
[ -e "$f2" ] || f2="../$f2"
[ -e "$f2" ] || {
echo could not resolve "$f1"
exit 1
}
rm "$f1"
cp -pv "$f2" "$f1"
); done
# insert asynchat
mkdir copyparty/vend
for n in asyncore.py asynchat.py; do

View file

@ -25,6 +25,7 @@ copyparty/res,
copyparty/res/COPYING.txt,
copyparty/res/insecure.pem,
copyparty/smbd.py,
copyparty/ssdp.py,
copyparty/star.py,
copyparty/stolen,
copyparty/stolen/__init__.py,
@ -57,6 +58,9 @@ copyparty/vend,
copyparty/vend/asynchat.py,
copyparty/vend/asyncore.py,
copyparty/web,
copyparty/web/a,
copyparty/web/a/up2k.py,
copyparty/web/a/webdav-cfg.bat,
copyparty/web/baguettebox.js,
copyparty/web/browser.css,
copyparty/web/browser.html,
@ -94,6 +98,8 @@ copyparty/web/msg.html,
copyparty/web/splash.css,
copyparty/web/splash.html,
copyparty/web/splash.js,
copyparty/web/svcs.html,
copyparty/web/svcs.js,
copyparty/web/ui.css,
copyparty/web/up2k.js,
copyparty/web/util.js,