Compare commits

...

43 Commits

Author SHA1 Message Date
ed
93ed0fc10b v0.6.3 2021-01-07 01:09:32 +01:00
ed
0d98cefd40 fix dumb 2021-01-07 01:06:31 +01:00
ed
d58988a033 use sendfile when possible 2021-01-07 00:50:42 +01:00
ed
2acfab1e3f cleanup 2021-01-06 22:54:54 +01:00
ed
b915dfe9a6 nagle adds ~.2sec delay on last packet 2021-01-06 21:08:52 +00:00
ed
25bd5a823e fuse-client: add timestamps to logger 2021-01-06 17:40:42 +01:00
ed
1c35de4716 fuse-client: cache tweaks 2021-01-06 17:22:07 +01:00
ed
4c00435a0a fuse: add windows-explorer settings 2021-01-06 17:18:37 +01:00
ed
844e3079a8 saved for posterity 2021-01-06 17:13:24 +01:00
ed
4778cb5b2c readme: add quickstart 2021-01-02 22:57:48 +01:00
ed
ec5d60b919 fuse-client: fix directory parser 2021-01-01 21:54:56 +01:00
ed
e1f4b960e8 oh no 2020-12-20 02:33:37 +01:00
ed
669e46da54 update TODOs 2020-12-14 09:19:43 +01:00
ed
ba94cc5df7 v0.6.2 2020-12-14 04:28:21 +01:00
ed
d08245c3df v0.6.1 2020-12-14 03:51:24 +01:00
ed
5c18d12cbf self-upgrading upgrader... getting too meta 2020-12-14 03:45:59 +01:00
ed
580a42dec7 sfx-repack: support wget 2020-12-14 02:59:15 +01:00
ed
29286e159b up2k-client: ignore rejected dupes 2020-12-12 00:55:42 +01:00
ed
19bcf90e9f support uploads with huge filenames 2020-12-12 00:35:54 +01:00
ed
dae9c00742 always display world-readable subvolumes 2020-12-04 23:28:18 +01:00
ed
35324ceb7c tests: support windows 2020-12-04 23:26:46 +01:00
ed
5aadd47199 dodge python-bug #7980 2020-12-01 23:20:44 +01:00
ed
7d9057cc62 v0.6.0 2020-12-01 02:58:11 +01:00
ed
c4b322b883 this commit sponsored by eslint 2020-12-01 02:25:46 +01:00
ed
19b09c898a fix sfx repack whoops 2020-11-30 03:27:27 +01:00
ed
eafe2098b6 v0.5.7 2020-11-30 03:01:14 +01:00
ed
2bc6a20d71 md: poll server for changes 2020-11-30 03:00:44 +01:00
ed
8b502a7235 v0.5.6 2020-11-29 19:49:16 +01:00
ed
37567844af md: add render2 plugin func 2020-11-29 19:34:08 +01:00
ed
2f6c4e0e34 refactoring 2020-11-29 19:32:22 +01:00
ed
1c7cc4cb2b ignore border when sizing table 2020-11-29 18:48:55 +01:00
ed
f83db3648e git tag as sfx version 2020-11-28 20:02:20 +01:00
ed
b164aa00d4 md: fix eof scroll glitch 2020-11-27 21:25:52 +01:00
ed
a2d866d0c2 show plugin errors 2020-11-27 21:10:47 +01:00
ed
2dfe4ac4c6 v0.5.5 2020-11-27 03:25:14 +01:00
ed
db65d05cb5 fix unittest for recent macos versions 2020-11-27 03:24:55 +01:00
ed
300c0194c7 add inline markdown plugins 2020-11-27 03:22:41 +01:00
ed
37a0d2b087 good idea 2020-11-19 02:24:26 +01:00
ed
a4959300ea add sfx downloader/repacker 2020-11-19 01:23:24 +01:00
ed
223657e5f8 v0.5.4 2020-11-17 23:58:08 +01:00
ed
0c53de6767 more lenient md table formatter 2020-11-17 23:55:14 +01:00
ed
9c309b1498 add filetype column 2020-11-17 23:43:55 +01:00
ed
1aa1b34c80 add reverse-proxy support 2020-11-17 23:42:33 +01:00
37 changed files with 2413 additions and 445 deletions

12
.eslintrc.json Normal file
View File

@@ -0,0 +1,12 @@
{
"env": {
"browser": true,
"es2021": true
},
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": 12
},
"rules": {
}
}

2
.gitattributes vendored
View File

@@ -1,4 +1,6 @@
* text eol=lf * text eol=lf
*.reg text eol=crlf
*.png binary *.png binary
*.gif binary *.gif binary

2
.vscode/launch.json vendored
View File

@@ -10,6 +10,8 @@
"cwd": "${workspaceFolder}", "cwd": "${workspaceFolder}",
"args": [ "args": [
//"-nw", //"-nw",
"-ed",
"-emp",
"-a", "-a",
"ed:wark", "ed:wark",
"-v", "-v",

View File

@@ -13,6 +13,17 @@ turn your phone or raspi into a portable file server with resumable uploads/down
* code standard: `black` * code standard: `black`
## quickstart
download [copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py) and you're all set!
running the sfx without arguments (for example doubleclicking it on Windows) will let anyone access the current folder; see `-h` for help if you want accounts and volumes etc
you may also want these, especially on servers:
* [contrib/systemd/copyparty.service](contrib/systemd/copyparty.service) to run copyparty as a systemd service
* [contrib/nginx/copyparty.conf](contrib/nginx/copyparty.conf) to reverse-proxy behind nginx (for legit https)
## notes ## notes
* iPhone/iPad: use Firefox to download files * iPhone/iPad: use Firefox to download files
@@ -87,16 +98,18 @@ the features you can opt to drop are
for the `re`pack to work, first run one of the sfx'es once to unpack it for the `re`pack to work, first run one of the sfx'es once to unpack it
**note:** you can also just download and run [scripts/copyparty-repack.sh](scripts/copyparty-repack.sh) -- this will grab the latest copyparty release from github and do a `no-ogv no-cm` repack; works on linux/macos (and windows with msys2 or WSL)
# install on android # install on android
install [Termux](https://termux.com/) (see [ocv.me/termux](https://ocv.me/termux/)) and then copy-paste this into Termux (long-tap) all at once: install [Termux](https://termux.com/) (see [ocv.me/termux](https://ocv.me/termux/)) and then copy-paste this into Termux (long-tap) all at once:
```sh ```sh
apt update && apt -y full-upgrade && termux-setup-storage && apt -y install curl && cd && curl -L https://github.com/9001/copyparty/raw/master/scripts/copyparty-android.sh > copyparty-android.sh && chmod 755 copyparty-android.sh && ./copyparty-android.sh -h apt update && apt -y full-upgrade && termux-setup-storage && apt -y install python && python -m ensurepip && python -m pip install -U copyparty
echo $? echo $?
``` ```
after the initial setup (and restarting bash), you can launch copyparty at any time by running "copyparty" in Termux after the initial setup, you can launch copyparty at any time by running `copyparty` anywhere in Termux
# dev env setup # dev env setup
@@ -124,13 +137,16 @@ in the `scripts` folder:
roughly sorted by priority roughly sorted by priority
* up2k handle filename too long * reduce up2k roundtrips
* up2k fails on empty files? alert then stuck * start from a chunk index and just go
* terminate client on bad data
* drop onto folders * drop onto folders
* look into android thumbnail cache file format * `os.copy_file_range` for up2k cloning
* support pillow-simd * support pillow-simd
* cache sha512 chunks on client * cache sha512 chunks on client
* symlink existing files on upload * ~~symlink existing files on upload~~
* ok at runtime, up2k db still not persisted
* comment field * comment field
* ~~look into android thumbnail cache file format~~ bad idea
* figure out the deal with pixel3a not being connectable as hotspot * figure out the deal with pixel3a not being connectable as hotspot
* pixel3a having unpredictable 3sec latency in general :|||| * pixel3a having unpredictable 3sec latency in general :||||

View File

@@ -34,3 +34,8 @@ you could replace winfsp with [dokan](https://github.com/dokan-dev/dokany/releas
* does the same thing except more correct, `samba` approves * does the same thing except more correct, `samba` approves
* **supports Linux** -- expect `18 MiB/s` (wait what) * **supports Linux** -- expect `18 MiB/s` (wait what)
* **supports Macos** -- probably * **supports Macos** -- probably
# copyparty-fuse-streaming.py
* pretend this doesn't exist

1100
bin/copyparty-fuse-streaming.py Executable file

File diff suppressed because it is too large Load Diff

View File

@@ -12,7 +12,7 @@ __url__ = "https://github.com/9001/copyparty/"
mount a copyparty server (local or remote) as a filesystem mount a copyparty server (local or remote) as a filesystem
usage: usage:
python copyparty-fuse.py ./music http://192.168.1.69:3923/ python copyparty-fuse.py http://192.168.1.69:3923/ ./music
dependencies: dependencies:
python3 -m pip install --user fusepy python3 -m pip install --user fusepy
@@ -20,6 +20,10 @@ dependencies:
+ on Macos: https://osxfuse.github.io/ + on Macos: https://osxfuse.github.io/
+ on Windows: https://github.com/billziss-gh/winfsp/releases/latest + on Windows: https://github.com/billziss-gh/winfsp/releases/latest
note:
you probably want to run this on windows clients:
https://github.com/9001/copyparty/blob/master/contrib/explorer-nothumbs-nofoldertypes.reg
get server cert: get server cert:
awk '/-BEGIN CERTIFICATE-/ {a=1} a; /-END CERTIFICATE-/{exit}' <(openssl s_client -connect 127.0.0.1:3923 </dev/null 2>/dev/null) >cert.pem awk '/-BEGIN CERTIFICATE-/ {a=1} a; /-END CERTIFICATE-/{exit}' <(openssl s_client -connect 127.0.0.1:3923 </dev/null 2>/dev/null) >cert.pem
""" """
@@ -100,7 +104,7 @@ def rice_tid():
def fancy_log(msg): def fancy_log(msg):
print("{} {}\n".format(rice_tid(), msg), end="") print("{:10.6f} {} {}\n".format(time.time() % 900, rice_tid(), msg), end="")
def null_log(msg): def null_log(msg):
@@ -159,7 +163,7 @@ class RecentLog(object):
thr.start() thr.start()
def put(self, msg): def put(self, msg):
msg = "{} {}\n".format(rice_tid(), msg) msg = "{:10.6f} {} {}\n".format(time.time() % 900, rice_tid(), msg)
if self.f: if self.f:
fmsg = " ".join([datetime.utcnow().strftime("%H%M%S.%f"), str(msg)]) fmsg = " ".join([datetime.utcnow().strftime("%H%M%S.%f"), str(msg)])
self.f.write(fmsg.encode("utf-8")) self.f.write(fmsg.encode("utf-8"))
@@ -367,7 +371,7 @@ class Gateway(object):
ret = [] ret = []
remainder = b"" remainder = b""
ptn = re.compile( ptn = re.compile(
r'^<tr><td>(-|DIR)</td><td><a[^>]* href="([^"]+)"[^>]*>([^<]+)</a></td><td>([^<]+)</td><td>([^<]+)</td></tr>$' r'^<tr><td>(-|DIR|<a [^<]+</a>)</td><td><a[^>]* href="([^"]+)"[^>]*>([^<]+)</a></td><td>([^<]+)</td><td>[^<]+</td><td>([^<]+)</td></tr>$'
) )
while True: while True:
@@ -405,7 +409,7 @@ class Gateway(object):
info("bad HTML or OS [{}] [{}]".format(fdate, fsize)) info("bad HTML or OS [{}] [{}]".format(fdate, fsize))
# python cannot strptime(1959-01-01) on windows # python cannot strptime(1959-01-01) on windows
if ftype == "-": if ftype != "DIR":
ret.append([fname, self.stat_file(ts, sz), 0]) ret.append([fname, self.stat_file(ts, sz), 0])
else: else:
ret.append([fname, self.stat_dir(ts, sz), 0]) ret.append([fname, self.stat_dir(ts, sz), 0])
@@ -658,8 +662,18 @@ class CPPF(Operations):
else: else:
if get2 - get1 <= 1024 * 1024: if get2 - get1 <= 1024 * 1024:
h_ofs = get1 - 256 * 1024 # unless the request is for the last n bytes of the file,
h_end = get2 + 1024 * 1024 # grow the start to cache some stuff around the range
if get2 < file_sz - 1:
h_ofs = get1 - 1024 * 256
else:
h_ofs = get1 - 1024 * 32
# likewise grow the end unless start is 0
if get1 > 0:
h_end = get2 + 1024 * 1024
else:
h_end = get2 + 1024 * 64
else: else:
# big enough, doesn't need pads # big enough, doesn't need pads
h_ofs = get1 h_ofs = get1
@@ -705,6 +719,7 @@ class CPPF(Operations):
self.dircache.append(cn) self.dircache.append(cn)
self.clean_dircache() self.clean_dircache()
# import pprint; pprint.pprint(ret)
return ret return ret
def readdir(self, path, fh=None): def readdir(self, path, fh=None):
@@ -802,7 +817,11 @@ class CPPF(Operations):
# dbg("=" + repr(cache_stat)) # dbg("=" + repr(cache_stat))
return cache_stat return cache_stat
info("=ENOENT ({})".format(hexler(path))) fun = info
if MACOS and path.split('/')[-1].startswith('._'):
fun = dbg
fun("=ENOENT ({})".format(hexler(path)))
raise FuseOSError(errno.ENOENT) raise FuseOSError(errno.ENOENT)
access = None access = None
@@ -906,6 +925,7 @@ class TheArgparseFormatter(
def main(): def main():
global info, log, dbg global info, log, dbg
time.strptime("19970815", "%Y%m%d") # python#7980
# filecache helps for reads that are ~64k or smaller; # filecache helps for reads that are ~64k or smaller;
# linux generally does 128k so the cache is a slowdown, # linux generally does 128k so the cache is a slowdown,

View File

@@ -567,6 +567,8 @@ class CPPF(Fuse):
def main(): def main():
time.strptime("19970815", "%Y%m%d") # python#7980
server = CPPF() server = CPPF()
server.parser.add_option(mountopt="url", metavar="BASE_URL", default=None) server.parser.add_option(mountopt="url", metavar="BASE_URL", default=None)
server.parse(values=server, errex=1) server.parse(values=server, errex=1)

View File

@@ -9,7 +9,14 @@
* assumes the webserver and copyparty is running on the same server/IP * assumes the webserver and copyparty is running on the same server/IP
* modify `10.13.1.1` as necessary if you wish to support browsers without javascript * modify `10.13.1.1` as necessary if you wish to support browsers without javascript
### [`explorer-nothumbs-nofoldertypes.reg`](explorer-nothumbs-nofoldertypes.reg)
disables thumbnails and folder-type detection in windows explorer, makes it way faster (especially for slow/networked locations (such as copyparty-fuse))
# OS integration # OS integration
init-scripts to start copyparty as a service init-scripts to start copyparty as a service
* [`systemd/copyparty.service`](systemd/copyparty.service) * [`systemd/copyparty.service`](systemd/copyparty.service)
* [`openrc/copyparty`](openrc/copyparty) * [`openrc/copyparty`](openrc/copyparty)
# Reverse-proxy
copyparty has basic support for running behind another webserver
* [`nginx/copyparty.conf`](nginx/copyparty.conf)

View File

@@ -0,0 +1,31 @@
Windows Registry Editor Version 5.00
; this will do 3 things, all optional:
; 1) disable thumbnails
; 2) delete all existing folder type settings/detections
; 3) disable folder type detection (force default columns)
;
; this makes the file explorer way faster,
; especially on slow/networked locations
; =====================================================================
; 1) disable thumbnails
[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced]
"IconsOnly"=dword:00000001
; =====================================================================
; 2) delete all existing folder type settings/detections
[-HKEY_CURRENT_USER\Software\Classes\Local Settings\Software\Microsoft\Windows\Shell\Bags]
[-HKEY_CURRENT_USER\Software\Classes\Local Settings\Software\Microsoft\Windows\Shell\BagMRU]
; =====================================================================
; 3) disable folder type detection
[HKEY_CURRENT_USER\Software\Classes\Local Settings\Software\Microsoft\Windows\Shell\Bags\AllFolders\Shell]
"FolderType"="NotSpecified"

View File

@@ -0,0 +1,26 @@
upstream cpp {
server 127.0.0.1:3923;
keepalive 120;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name fs.example.com;
location / {
proxy_pass http://cpp;
proxy_redirect off;
# disable buffering (next 4 lines)
proxy_http_version 1.1;
client_max_body_size 0;
proxy_buffering off;
proxy_request_buffering off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Connection "Keep-Alive";
}
}

View File

@@ -9,6 +9,7 @@ __license__ = "MIT"
__url__ = "https://github.com/9001/copyparty/" __url__ = "https://github.com/9001/copyparty/"
import os import os
import time
import shutil import shutil
import filecmp import filecmp
import locale import locale
@@ -85,6 +86,7 @@ def ensure_cert():
def main(): def main():
time.strptime("19970815", "%Y%m%d") # python#7980
if WINDOWS: if WINDOWS:
os.system("") # enables colors os.system("") # enables colors
@@ -123,22 +125,21 @@ def main():
""" """
), ),
) )
ap.add_argument( ap.add_argument("-c", metavar="PATH", type=str, action="append", help="add config file")
"-c", metavar="PATH", type=str, action="append", help="add config file"
)
ap.add_argument("-i", metavar="IP", type=str, default="0.0.0.0", help="ip to bind") ap.add_argument("-i", metavar="IP", type=str, default="0.0.0.0", help="ip to bind")
ap.add_argument("-p", metavar="PORT", type=int, default=3923, help="port to bind") ap.add_argument("-p", metavar="PORT", type=int, default=3923, help="port to bind")
ap.add_argument("-nc", metavar="NUM", type=int, default=64, help="max num clients") ap.add_argument("-nc", metavar="NUM", type=int, default=64, help="max num clients")
ap.add_argument( ap.add_argument("-j", metavar="CORES", type=int, default=1, help="max num cpu cores")
"-j", metavar="CORES", type=int, default=1, help="max num cpu cores"
)
ap.add_argument("-a", metavar="ACCT", type=str, action="append", help="add account") ap.add_argument("-a", metavar="ACCT", type=str, action="append", help="add account")
ap.add_argument("-v", metavar="VOL", type=str, action="append", help="add volume") ap.add_argument("-v", metavar="VOL", type=str, action="append", help="add volume")
ap.add_argument("-q", action="store_true", help="quiet") ap.add_argument("-q", action="store_true", help="quiet")
ap.add_argument("-ed", action="store_true", help="enable ?dots") ap.add_argument("-ed", action="store_true", help="enable ?dots")
ap.add_argument("-emp", action="store_true", help="enable markdown plugins")
ap.add_argument("-mcr", metavar="SEC", type=int, default=60, help="md-editor mod-chk rate")
ap.add_argument("-nw", action="store_true", help="disable writes (benchmark)") 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("-nih", action="store_true", help="no info hostname")
ap.add_argument("-nid", action="store_true", help="no info disk-usage") ap.add_argument("-nid", action="store_true", help="no info disk-usage")
ap.add_argument("--no-sendfile", action="store_true", help="disable sendfile")
al = ap.parse_args() al = ap.parse_args()
SvcHub(al).run() SvcHub(al).run()

View File

@@ -1,8 +1,8 @@
# coding: utf-8 # coding: utf-8
VERSION = (0, 5, 3) VERSION = (0, 6, 3)
CODENAME = "fuse jelly" CODENAME = "CHRISTMAAAAAS"
BUILD_DT = (2020, 11, 13) BUILD_DT = (2021, 1, 7)
S_VERSION = ".".join(map(str, VERSION)) S_VERSION = ".".join(map(str, VERSION))
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT) S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)

View File

@@ -104,7 +104,7 @@ class VFS(object):
real.sort() real.sort()
if not rem: if not rem:
for name, vn2 in sorted(self.nodes.items()): for name, vn2 in sorted(self.nodes.items()):
if uname in vn2.uread: if uname in vn2.uread or "*" in vn2.uread:
virt_vis[name] = vn2 virt_vis[name] = vn2
# no vfs nodes in the list of real inodes # no vfs nodes in the list of real inodes

View File

@@ -83,6 +83,10 @@ class HttpCli(object):
v = self.headers.get("connection", "").lower() v = self.headers.get("connection", "").lower()
self.keepalive = not v.startswith("close") self.keepalive = not v.startswith("close")
v = self.headers.get("x-forwarded-for", None)
if v is not None and self.conn.addr[0] in ["127.0.0.1", "::1"]:
self.log_src = self.conn.set_rproxy(v.split(",")[0])
self.uname = "*" self.uname = "*"
if "cookie" in self.headers: if "cookie" in self.headers:
cookies = self.headers["cookie"].split(";") cookies = self.headers["cookie"].split(";")
@@ -462,7 +466,7 @@ class HttpCli(object):
spd = self._spd(post_sz) spd = self._spd(post_sz)
self.log("{} thank".format(spd)) self.log("{} thank".format(spd))
self.reply("thank") self.reply(b"thank")
return True return True
def handle_login(self): def handle_login(self):
@@ -564,24 +568,24 @@ class HttpCli(object):
self.log("discarding incoming file without filename") self.log("discarding incoming file without filename")
# fallthrough # fallthrough
fn = os.devnull
if p_file and not nullwrite: if p_file and not nullwrite:
fdir = os.path.join(vfs.realpath, rem) fdir = os.path.join(vfs.realpath, rem)
fn = os.path.join(fdir, sanitize_fn(p_file)) fname = sanitize_fn(p_file)
if not os.path.isdir(fsenc(fdir)): if not os.path.isdir(fsenc(fdir)):
raise Pebkac(404, "that folder does not exist") raise Pebkac(404, "that folder does not exist")
# TODO broker which avoid this race and suffix = ".{:.6f}-{}".format(time.time(), self.addr[0])
# provides a new filename if taken (same as up2k) open_args = {"fdir": fdir, "suffix": suffix}
if os.path.exists(fsenc(fn)): else:
fn += ".{:.6f}-{}".format(time.time(), self.addr[0]) open_args = {}
# using current-time instead of t0 cause clients fname = os.devnull
# may reuse a name for multiple files in one post fdir = ""
try: try:
with open(fsenc(fn), "wb") as f: with ren_open(fname, "wb", 512 * 1024, **open_args) as f:
self.log("writing to {0}".format(fn)) f, fname = f["orz"]
self.log("writing to {}/{}".format(fdir, fname))
sz, sha512_hex, _ = hashcopy(self.conn, p_data, f) sz, sha512_hex, _ = hashcopy(self.conn, p_data, f)
if sz == 0: if sz == 0:
raise Pebkac(400, "empty files in post") raise Pebkac(400, "empty files in post")
@@ -590,8 +594,14 @@ class HttpCli(object):
self.conn.nbyte += sz self.conn.nbyte += sz
except Pebkac: except Pebkac:
if fn != os.devnull: if fname != os.devnull:
os.rename(fsenc(fn), fsenc(fn + ".PARTIAL")) fp = os.path.join(fdir, fname)
suffix = ".PARTIAL"
try:
os.rename(fsenc(fp), fsenc(fp + suffix))
except:
fp = fp[: -len(suffix)]
os.rename(fsenc(fp), fsenc(fp + suffix))
raise raise
@@ -676,7 +686,7 @@ class HttpCli(object):
return True return True
fp = os.path.join(vfs.realpath, rem) fp = os.path.join(vfs.realpath, rem)
srv_lastmod = -1 srv_lastmod = srv_lastmod3 = -1
try: try:
st = os.stat(fsenc(fp)) st = os.stat(fsenc(fp))
srv_lastmod = st.st_mtime srv_lastmod = st.st_mtime
@@ -727,7 +737,7 @@ class HttpCli(object):
if p_field != "body": if p_field != "body":
raise Pebkac(400, "expected body, got {}".format(p_field)) raise Pebkac(400, "expected body, got {}".format(p_field))
with open(fp, "wb") as f: with open(fp, "wb", 512 * 1024) as f:
sz, sha512, _ = hashcopy(self.conn, p_data, f) sz, sha512, _ = hashcopy(self.conn, p_data, f)
new_lastmod = os.stat(fsenc(fp)).st_mtime new_lastmod = os.stat(fsenc(fp)).st_mtime
@@ -752,9 +762,12 @@ class HttpCli(object):
cli_dt = time.strptime(cli_lastmod, "%a, %d %b %Y %H:%M:%S GMT") cli_dt = time.strptime(cli_lastmod, "%a, %d %b %Y %H:%M:%S GMT")
cli_ts = calendar.timegm(cli_dt) cli_ts = calendar.timegm(cli_dt)
return file_lastmod, int(file_ts) > int(cli_ts) return file_lastmod, int(file_ts) > int(cli_ts)
except: except Exception as ex:
self.log("bad lastmod format: {}".format(cli_lastmod)) self.log(
self.log(" expected format: {}".format(file_lastmod)) "lastmod {}\nremote: [{}]\n local: [{}]".format(
repr(ex), cli_lastmod, file_lastmod
)
)
return file_lastmod, file_lastmod != cli_lastmod return file_lastmod, file_lastmod != cli_lastmod
return file_lastmod, True return file_lastmod, True
@@ -871,6 +884,7 @@ class HttpCli(object):
logtail += " [\033[36m{}-{}\033[0m]".format(lower, upper) logtail += " [\033[36m{}-{}\033[0m]".format(lower, upper)
use_sendfile = False
if decompress: if decompress:
open_func = gzip.open open_func = gzip.open
open_args = [fsenc(fs_path), "rb"] open_args = [fsenc(fs_path), "rb"]
@@ -880,6 +894,8 @@ class HttpCli(object):
open_func = open open_func = open
# 512 kB is optimal for huge files, use 64k # 512 kB is optimal for huge files, use 64k
open_args = [fsenc(fs_path), "rb", 64 * 1024] open_args = [fsenc(fs_path), "rb", 64 * 1024]
if hasattr(os, 'sendfile'):
use_sendfile = not self.args.no_sendfile
# #
# send reply # send reply
@@ -902,24 +918,13 @@ class HttpCli(object):
ret = True ret = True
with open_func(*open_args) as f: with open_func(*open_args) as f:
remains = upper - lower if use_sendfile:
f.seek(lower) remains = sendfile_kern(lower, upper, f, self.s)
while remains > 0: else:
# time.sleep(0.01) remains = sendfile_py(lower, upper, f, self.s)
buf = f.read(4096)
if not buf:
break
if remains < len(buf): if remains > 0:
buf = buf[:remains] logmsg += " \033[31m" + str(upper - remains) + "\033[0m"
try:
self.s.sendall(buf)
remains -= len(buf)
except:
logmsg += " \033[31m" + str(upper - remains) + "\033[0m"
ret = False
break
spd = self._spd((upper - lower) - remains) spd = self._spd((upper - lower) - remains)
self.log("{}, {}".format(logmsg, spd)) self.log("{}, {}".format(logmsg, spd))
@@ -959,6 +964,8 @@ class HttpCli(object):
"edit": "edit" in self.uparam, "edit": "edit" in self.uparam,
"title": html_escape(self.vpath), "title": html_escape(self.vpath),
"lastmod": int(ts_md * 1000), "lastmod": int(ts_md * 1000),
"md_plug": "true" if self.args.emp else "false",
"md_chk_rate": self.args.mcr,
"md": "", "md": "",
} }
sz_html = len(template.render(**targs).encode("utf-8")) sz_html = len(template.render(**targs).encode("utf-8"))
@@ -1073,7 +1080,12 @@ class HttpCli(object):
dt = datetime.utcfromtimestamp(inf.st_mtime) dt = datetime.utcfromtimestamp(inf.st_mtime)
dt = dt.strftime("%Y-%m-%d %H:%M:%S") dt = dt.strftime("%Y-%m-%d %H:%M:%S")
item = [margin, quotep(href), html_escape(fn), sz, dt] try:
ext = "---" if is_dir else fn.rsplit(".", 1)[1]
except:
ext = "%"
item = [margin, quotep(href), html_escape(fn), sz, ext, dt]
if is_dir: if is_dir:
dirs.append(item) dirs.append(item)
else: else:

View File

@@ -46,7 +46,7 @@ class HttpConn(object):
self.nbyte = 0 self.nbyte = 0
self.workload = 0 self.workload = 0
self.log_func = hsrv.log self.log_func = hsrv.log
self.log_src = "{} \033[36m{}".format(addr[0], addr[1]).ljust(26) self.set_rproxy()
env = jinja2.Environment() env = jinja2.Environment()
env.loader = jinja2.FileSystemLoader(os.path.join(E.mod, "web")) env.loader = jinja2.FileSystemLoader(os.path.join(E.mod, "web"))
@@ -56,6 +56,18 @@ class HttpConn(object):
self.tpl_md = env.get_template("md.html") self.tpl_md = env.get_template("md.html")
self.tpl_mde = env.get_template("mde.html") self.tpl_mde = env.get_template("mde.html")
def set_rproxy(self, ip=None):
if ip is None:
color = 36
ip = self.addr[0]
self.rproxy = None
else:
color = 34
self.rproxy = ip
self.log_src = "{} \033[{}m{}".format(ip, color, self.addr[1]).ljust(26)
return self.log_src
def respath(self, res_name): def respath(self, res_name):
return os.path.join(E.mod, "web", res_name) return os.path.join(E.mod, "web", res_name)

View File

@@ -36,6 +36,7 @@ class TcpSrv(object):
self.srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.srv.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
try: try:
self.srv.bind((self.args.i, self.args.p)) self.srv.bind((self.args.i, self.args.p))
except (OSError, socket.error) as ex: except (OSError, socket.error) as ex:

View File

@@ -13,7 +13,7 @@ import threading
from copy import deepcopy from copy import deepcopy
from .__init__ import WINDOWS from .__init__ import WINDOWS
from .util import Pebkac, Queue, fsenc, sanitize_fn from .util import Pebkac, Queue, fsenc, sanitize_fn, ren_open
class Up2k(object): class Up2k(object):
@@ -68,9 +68,11 @@ class Up2k(object):
# symlink to the client-provided name, # symlink to the client-provided name,
# returning the previous upload info # returning the previous upload info
job = deepcopy(job) job = deepcopy(job)
suffix = self._suffix(dst, now, job["addr"]) job["rdir"] = cj["rdir"]
job["name"] = cj["name"] + suffix job["name"] = self._untaken(cj["rdir"], cj["name"], now, cj["addr"])
self._symlink(src, dst + suffix) dst = os.path.join(job["rdir"], job["name"])
os.unlink(fsenc(dst)) # TODO ed pls
self._symlink(src, dst)
else: else:
job = { job = {
"wark": wark, "wark": wark,
@@ -85,9 +87,6 @@ class Up2k(object):
"hash": deepcopy(cj["hash"]), "hash": deepcopy(cj["hash"]),
} }
path = os.path.join(job["rdir"], job["name"])
job["name"] += self._suffix(path, now, cj["addr"])
# one chunk may occur multiple times in a file; # one chunk may occur multiple times in a file;
# filter to unique values for the list of missing chunks # filter to unique values for the list of missing chunks
# (preserve order to reduce disk thrashing) # (preserve order to reduce disk thrashing)
@@ -108,13 +107,12 @@ class Up2k(object):
"wark": wark, "wark": wark,
} }
def _suffix(self, fpath, ts, ip): def _untaken(self, fdir, fname, ts, ip):
# TODO broker which avoid this race and # TODO broker which avoid this race and
# provides a new filename if taken (same as bup) # provides a new filename if taken (same as bup)
if not os.path.exists(fsenc(fpath)): suffix = ".{:.6f}-{}".format(ts, ip)
return "" with ren_open(fname, "wb", fdir=fdir, suffix=suffix) as f:
return f["orz"][1]
return ".{:.6f}-{}".format(ts, ip)
def _symlink(self, src, dst): def _symlink(self, src, dst):
# TODO store this in linktab so we never delete src if there are links to it # TODO store this in linktab so we never delete src if there are links to it
@@ -141,7 +139,7 @@ class Up2k(object):
lsrc = "../" * (len(lsrc) - 1) + "/".join(lsrc) lsrc = "../" * (len(lsrc) - 1) + "/".join(lsrc)
os.symlink(fsenc(lsrc), fsenc(ldst)) os.symlink(fsenc(lsrc), fsenc(ldst))
except (AttributeError, OSError) as ex: except (AttributeError, OSError) as ex:
self.log("up2k", "cannot symlink; creating copy") self.log("up2k", "cannot symlink; creating copy: " + repr(ex))
shutil.copy2(fsenc(src), fsenc(dst)) shutil.copy2(fsenc(src), fsenc(dst))
def handle_chunk(self, wark, chash): def handle_chunk(self, wark, chash):
@@ -218,8 +216,9 @@ class Up2k(object):
def _new_upload(self, job): def _new_upload(self, job):
self.registry[job["wark"]] = job self.registry[job["wark"]] = job
path = os.path.join(job["rdir"], job["name"]) suffix = ".{:.6f}-{}".format(job["t0"], job["addr"])
with open(fsenc(path), "wb") as f: with ren_open(job["name"], "wb", fdir=job["rdir"], suffix=suffix) as f:
f, job["name"] = f["orz"]
f.seek(job["size"] - 1) f.seek(job["size"] - 1)
f.write(b"e") f.write(b"e")

View File

@@ -2,14 +2,17 @@
from __future__ import print_function, unicode_literals from __future__ import print_function, unicode_literals
import re import re
import os
import sys import sys
import time import time
import base64 import base64
import select
import struct import struct
import hashlib import hashlib
import platform import platform
import threading import threading
import mimetypes import mimetypes
import contextlib
import subprocess as sp # nosec import subprocess as sp # nosec
from .__init__ import PY2, WINDOWS from .__init__ import PY2, WINDOWS
@@ -96,6 +99,80 @@ class Unrecv(object):
self.buf = buf + self.buf self.buf = buf + self.buf
@contextlib.contextmanager
def ren_open(fname, *args, **kwargs):
fdir = kwargs.pop("fdir", None)
suffix = kwargs.pop("suffix", None)
if fname == os.devnull:
with open(fname, *args, **kwargs) as f:
yield {"orz": [f, fname]}
return
orig_name = fname
bname = fname
ext = ""
while True:
ofs = bname.rfind(".")
if ofs < 0 or ofs < len(bname) - 7:
# doesn't look like an extension anymore
break
ext = bname[ofs:] + ext
bname = bname[:ofs]
b64 = ""
while True:
try:
if fdir:
fpath = os.path.join(fdir, fname)
else:
fpath = fname
if suffix and os.path.exists(fpath):
fpath += suffix
fname += suffix
ext += suffix
with open(fsenc(fpath), *args, **kwargs) as f:
if b64:
fp2 = "fn-trunc.{}.txt".format(b64)
fp2 = os.path.join(fdir, fp2)
with open(fsenc(fp2), "wb") as f2:
f2.write(orig_name.encode("utf-8"))
yield {"orz": [f, fname]}
return
except OSError as ex_:
ex = ex_
if ex.errno != 36:
raise
if not b64:
b64 = (bname + ext).encode("utf-8", "replace")
b64 = hashlib.sha512(b64).digest()[:12]
b64 = base64.urlsafe_b64encode(b64).decode("utf-8").rstrip("=")
badlen = len(fname)
while len(fname) >= badlen:
if len(bname) < 8:
raise ex
if len(bname) > len(ext):
# drop the last letter of the filename
bname = bname[:-1]
else:
try:
# drop the leftmost sub-extension
_, ext = ext.split(".", 1)
except:
# okay do the first letter then
ext = "." + ext[2:]
fname = "{}~{}{}".format(bname, b64, ext)
class MultipartParser(object): class MultipartParser(object):
def __init__(self, log_func, sr, http_headers): def __init__(self, log_func, sr, http_headers):
self.sr = sr self.sr = sr
@@ -515,6 +592,46 @@ def hashcopy(actor, fin, fout):
return tlen, hashobj.hexdigest(), digest_b64 return tlen, hashobj.hexdigest(), digest_b64
def sendfile_py(lower, upper, f, s):
remains = upper - lower
f.seek(lower)
while remains > 0:
# time.sleep(0.01)
buf = f.read(min(4096, remains))
if not buf:
return remains
try:
s.sendall(buf)
remains -= len(buf)
except:
return remains
return 0
def sendfile_kern(lower, upper, f, s):
out_fd = s.fileno()
in_fd = f.fileno()
ofs = lower
while ofs < upper:
try:
req = min(2 ** 30, upper - ofs)
select.select([], [out_fd], [], 10)
n = os.sendfile(out_fd, in_fd, ofs, req)
except Exception as ex:
# print("sendfile: " + repr(ex))
n = 0
if n <= 0:
return upper - ofs
ofs += n
# print("sendfile: ok, sent {} now, {} total, {} remains".format(n, ofs - lower, upper - ofs))
return 0
def unescape_cookie(orig): def unescape_cookie(orig):
# mw=idk; doot=qwe%2Crty%3Basd+fgh%2Bjkl%25zxc%26vbn # qwe,rty;asd fgh+jkl%zxc&vbn # mw=idk; doot=qwe%2Crty%3Basd+fgh%2Bjkl%25zxc%26vbn # qwe,rty;asd fgh+jkl%zxc&vbn
ret = "" ret = ""

12
copyparty/web/Makefile Normal file
View File

@@ -0,0 +1,12 @@
# run me to zopfli all the static files
# which should help on really slow connections
# but then why are you using copyparty in the first place
pk: $(addsuffix .gz, $(wildcard *.js *.css))
un: $(addsuffix .un, $(wildcard *.gz))
%.gz: %
pigz -11 -J 34 -I 5730 $<
%.un: %
pigz -d $<

View File

@@ -34,13 +34,14 @@
<th></th> <th></th>
<th>File Name</th> <th>File Name</th>
<th sort="int">File Size</th> <th sort="int">File Size</th>
<th>T</th>
<th>Date</th> <th>Date</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{%- for f in files %} {%- for f in files %}
<tr><td>{{ f[0] }}</td><td><a href="{{ f[1] }}">{{ f[2] }}</a></td><td>{{ f[3] }}</td><td>{{ f[4] }}</td></tr> <tr><td>{{ f[0] }}</td><td><a href="{{ f[1] }}">{{ f[2] }}</a></td><td>{{ f[3] }}</td><td>{{ f[4] }}</td><td>{{ f[5] }}</td></tr>
{%- endfor %} {%- endfor %}
</tbody> </tbody>
@@ -67,6 +68,8 @@
</div> </div>
</div> </div>
<script src="/.cpr/util.js{{ ts }}"></script>
{%- if can_read %} {%- if can_read %}
<script src="/.cpr/browser.js{{ ts }}"></script> <script src="/.cpr/browser.js{{ ts }}"></script>
{%- endif %} {%- endif %}

View File

@@ -1,75 +1,9 @@
"use strict"; "use strict";
// error handler for mobile devices window.onerror = vis_exh;
function hcroak(msg) {
document.body.innerHTML = msg;
window.onerror = undefined;
throw 'fatal_err';
}
function croak(msg) {
document.body.textContent = msg;
window.onerror = undefined;
throw msg;
}
function esc(txt) {
return txt.replace(/[&"<>]/g, function (c) {
return {
'&': '&amp;',
'"': '&quot;',
'<': '&lt;',
'>': '&gt;'
}[c];
});
}
window.onerror = function (msg, url, lineNo, columnNo, error) {
window.onerror = undefined;
var html = ['<h1>you hit a bug!</h1><p>please screenshot this error and send me a copy arigathanks gozaimuch (ed/irc.rizon.net or ed#2644)</p><p>',
esc(String(msg)), '</p><p>', esc(url + ' @' + lineNo + ':' + columnNo), '</p>'];
if (error) {
var find = ['desc', 'stack', 'trace'];
for (var a = 0; a < find.length; a++)
if (String(error[find[a]]) !== 'undefined')
html.push('<h2>' + find[a] + '</h2>' +
esc(String(error[find[a]])).replace(/\n/g, '<br />\n'));
}
document.body.style.fontSize = '0.8em';
document.body.style.padding = '0 1em 1em 1em';
hcroak(html.join('\n'));
};
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/endsWith
if (!String.prototype.endsWith) {
String.prototype.endsWith = function (search, this_len) {
if (this_len === undefined || this_len > this.length) {
this_len = this.length;
}
return this.substring(this_len - search.length, this_len) === search;
};
}
// https://stackoverflow.com/a/950146
function import_js(url, cb) {
var head = document.head || document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = url;
script.onreadystatechange = cb;
script.onload = cb;
head.appendChild(script);
}
function o(id) {
return document.getElementById(id);
}
function dbg(msg) { function dbg(msg) {
o('path').innerHTML = msg; ebi('path').innerHTML = msg;
} }
function ev(e) { function ev(e) {
@@ -78,40 +12,7 @@ function ev(e) {
return e; return e;
} }
makeSortable(ebi('files'));
function sortTable(table, col) {
var tb = table.tBodies[0], // use `<tbody>` to ignore `<thead>` and `<tfoot>` rows
th = table.tHead.rows[0].cells,
tr = Array.prototype.slice.call(tb.rows, 0),
i, reverse = th[col].className == 'sort1' ? -1 : 1;
for (var a = 0, thl = th.length; a < thl; a++)
th[a].className = '';
th[col].className = 'sort' + reverse;
var stype = th[col].getAttribute('sort');
tr = tr.sort(function (a, b) {
var v1 = a.cells[col].textContent.trim();
var v2 = b.cells[col].textContent.trim();
if (stype == 'int') {
v1 = parseInt(v1.replace(/,/g, ''));
v2 = parseInt(v2.replace(/,/g, ''));
return reverse * (v1 - v2);
}
return reverse * (v1.localeCompare(v2));
});
for (i = 0; i < tr.length; ++i) tb.appendChild(tr[i]);
}
function makeSortable(table) {
var th = table.tHead, i;
th && (th = th.rows[0]) && (th = th.cells);
if (th) i = th.length;
else return; // if no `<thead>` then do nothing
while (--i >= 0) (function (i) {
th[i].onclick = function () {
sortTable(table, i);
};
}(i));
}
makeSortable(o('files'));
// extract songs + add play column // extract songs + add play column
@@ -124,9 +25,9 @@ var mp = (function () {
'tracks': tracks, 'tracks': tracks,
'cover_url': '' 'cover_url': ''
}; };
var re_audio = new RegExp('\.(opus|ogg|m4a|aac|mp3|wav|flac)$', 'i'); var re_audio = /\.(opus|ogg|m4a|aac|mp3|wav|flac)$/i;
var trs = document.getElementById('files').getElementsByTagName('tbody')[0].getElementsByTagName('tr'); var trs = ebi('files').getElementsByTagName('tbody')[0].getElementsByTagName('tr');
for (var a = 0, aa = trs.length; a < aa; a++) { for (var a = 0, aa = trs.length; a < aa; a++) {
var tds = trs[a].getElementsByTagName('td'); var tds = trs[a].getElementsByTagName('td');
var link = tds[1].getElementsByTagName('a')[0]; var link = tds[1].getElementsByTagName('a')[0];
@@ -142,7 +43,7 @@ var mp = (function () {
} }
for (var a = 0, aa = tracks.length; a < aa; a++) for (var a = 0, aa = tracks.length; a < aa; a++)
o('trk' + a).onclick = ev_play; ebi('trk' + a).onclick = ev_play;
ret.vol = localStorage.getItem('vol'); ret.vol = localStorage.getItem('vol');
if (ret.vol !== null) if (ret.vol !== null)
@@ -169,8 +70,8 @@ var mp = (function () {
// toggle player widget // toggle player widget
var widget = (function () { var widget = (function () {
var ret = {}; var ret = {};
var widget = document.getElementById('widget'); var widget = ebi('widget');
var wtoggle = document.getElementById('wtoggle'); var wtoggle = ebi('wtoggle');
var touchmode = false; var touchmode = false;
var side_open = false; var side_open = false;
var was_paused = true; var was_paused = true;
@@ -199,7 +100,7 @@ var widget = (function () {
ret.paused = function (paused) { ret.paused = function (paused) {
if (was_paused != paused) { if (was_paused != paused) {
was_paused = paused; was_paused = paused;
o('bplay').innerHTML = paused ? '▶' : '⏸'; ebi('bplay').innerHTML = paused ? '▶' : '⏸';
} }
}; };
var click_handler = function (e) { var click_handler = function (e) {
@@ -223,8 +124,8 @@ var widget = (function () {
// buffer/position bar // buffer/position bar
var pbar = (function () { var pbar = (function () {
var r = {}; var r = {};
r.bcan = o('barbuf'); r.bcan = ebi('barbuf');
r.pcan = o('barpos'); r.pcan = ebi('barpos');
r.bctx = r.bcan.getContext('2d'); r.bctx = r.bcan.getContext('2d');
r.pctx = r.pcan.getContext('2d'); r.pctx = r.pcan.getContext('2d');
@@ -289,7 +190,7 @@ var pbar = (function () {
// volume bar // volume bar
var vbar = (function () { var vbar = (function () {
var r = {}; var r = {};
r.can = o('pvol'); r.can = ebi('pvol');
r.ctx = r.can.getContext('2d'); r.ctx = r.can.getContext('2d');
var bctx = r.ctx; var bctx = r.ctx;
@@ -386,7 +287,7 @@ var vbar = (function () {
else else
play(0); play(0);
}; };
o('bplay').onclick = function (e) { ebi('bplay').onclick = function (e) {
ev(e); ev(e);
if (mp.au) { if (mp.au) {
if (mp.au.paused) if (mp.au.paused)
@@ -397,15 +298,15 @@ var vbar = (function () {
else else
play(0); play(0);
}; };
o('bprev').onclick = function (e) { ebi('bprev').onclick = function (e) {
ev(e); ev(e);
bskip(-1); bskip(-1);
}; };
o('bnext').onclick = function (e) { ebi('bnext').onclick = function (e) {
ev(e); ev(e);
bskip(1); bskip(1);
}; };
o('barpos').onclick = function (e) { ebi('barpos').onclick = function (e) {
if (!mp.au) { if (!mp.au) {
//dbg((new Date()).getTime()); //dbg((new Date()).getTime());
return play(0); return play(0);
@@ -414,8 +315,12 @@ var vbar = (function () {
var rect = pbar.pcan.getBoundingClientRect(); var rect = pbar.pcan.getBoundingClientRect();
var x = e.clientX - rect.left; var x = e.clientX - rect.left;
var mul = x * 1.0 / rect.width; var mul = x * 1.0 / rect.width;
var seek = mp.au.duration * mul;
console.log('seek: ' + seek);
if (!isFinite(seek))
return;
mp.au.currentTime = mp.au.duration * mul; mp.au.currentTime = seek;
if (mp.au === mp.au_native) if (mp.au === mp.au_native)
// hack: ogv.js breaks on .play() during playback // hack: ogv.js breaks on .play() during playback
@@ -471,7 +376,7 @@ function ev_play(e) {
function setclass(id, clas) { function setclass(id, clas) {
o(id).setAttribute('class', clas); ebi(id).setAttribute('class', clas);
} }
@@ -567,7 +472,6 @@ function play(tid, call_depth) {
function evau_error(e) { function evau_error(e) {
var err = ''; var err = '';
var eplaya = (e && e.target) || (window.event && window.event.srcElement); var eplaya = (e && e.target) || (window.event && window.event.srcElement);
var url = eplaya.src;
switch (eplaya.error.code) { switch (eplaya.error.code) {
case eplaya.error.MEDIA_ERR_ABORTED: case eplaya.error.MEDIA_ERR_ABORTED:
@@ -608,26 +512,27 @@ function show_modal(html) {
// hide fullscreen message // hide fullscreen message
function unblocked() { function unblocked() {
var dom = o('blocked'); var dom = ebi('blocked');
if (dom) if (dom)
dom.parentNode.removeChild(dom); dom.parentNode.removeChild(dom);
} }
// show ui to manually start playback of a linked song // show ui to manually start playback of a linked song
function autoplay_blocked(tid) { function autoplay_blocked() {
show_modal( show_modal(
'<div id="blk_play"><a id="blk_go"></a></div>' + '<div id="blk_play"><a href="#" id="blk_go"></a></div>' +
'<div id="blk_abrt"><a id="blk_na">Cancel<br />(show file list)</a></div>'); '<div id="blk_abrt"><a href="#" id="blk_na">Cancel<br />(show file list)</a></div>');
var go = o('blk_go'); var go = ebi('blk_go');
var na = o('blk_na'); var na = ebi('blk_na');
var fn = mp.tracks[mp.au.tid].split(/\//).pop(); var fn = mp.tracks[mp.au.tid].split(/\//).pop();
fn = decodeURIComponent(fn.replace(/\+/g, ' ')); fn = decodeURIComponent(fn.replace(/\+/g, ' '));
go.textContent = 'Play "' + fn + '"'; go.textContent = 'Play "' + fn + '"';
go.onclick = function () { go.onclick = function (e) {
if (e) e.preventDefault();
unblocked(); unblocked();
mp.au.play(); mp.au.play();
}; };

View File

@@ -123,8 +123,12 @@ write markdown (most html is 🙆 too)
<script> <script>
var link_md_as_html = false; // TODO (does nothing)
var last_modified = {{ lastmod }}; var last_modified = {{ lastmod }};
var md_opt = {
link_md_as_html: false,
allow_plugins: {{ md_plug }},
modpoll_freq: {{ md_chk_rate }}
};
(function () { (function () {
var btn = document.getElementById("lightswitch"); var btn = document.getElementById("lightswitch");
@@ -141,17 +145,11 @@ var last_modified = {{ lastmod }};
toggle(); toggle();
})(); })();
if (!String.startsWith) {
String.prototype.startsWith = function(s, i) {
i = i>0 ? i|0 : 0;
return this.substring(i, i + s.length) === s;
};
}
</script> </script>
<script src="/.cpr/util.js"></script>
<script src="/.cpr/deps/marked.full.js"></script> <script src="/.cpr/deps/marked.full.js"></script>
<script src="/.cpr/md.js"></script> <script src="/.cpr/md.js"></script>
{%- if edit %} {%- if edit %}
<script src="/.cpr/md2.js"></script> <script src="/.cpr/md2.js"></script>
{%- endif %} {%- endif %}
</body></html> </body></html>

View File

@@ -1,10 +1,12 @@
var dom_toc = document.getElementById('toc'); "use strict";
var dom_wrap = document.getElementById('mw');
var dom_hbar = document.getElementById('mh'); var dom_toc = ebi('toc');
var dom_nav = document.getElementById('mn'); var dom_wrap = ebi('mw');
var dom_pre = document.getElementById('mp'); var dom_hbar = ebi('mh');
var dom_src = document.getElementById('mt'); var dom_nav = ebi('mn');
var dom_navtgl = document.getElementById('navtoggle'); var dom_pre = ebi('mp');
var dom_src = ebi('mt');
var dom_navtgl = ebi('navtoggle');
// chrome 49 needs this // chrome 49 needs this
@@ -18,6 +20,10 @@ var dbg = function () { };
// dbg = console.log // dbg = console.log
// plugins
var md_plug = {};
function hesc(txt) { function hesc(txt) {
return txt.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;"); return txt.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
} }
@@ -30,7 +36,7 @@ function cls(dom, name, add) {
} }
function static(obj) { function statify(obj) {
return JSON.parse(JSON.stringify(obj)); return JSON.parse(JSON.stringify(obj));
} }
@@ -154,13 +160,110 @@ function copydom(src, dst, lv) {
} }
function md_plug_err(ex, js) {
var errbox = ebi('md_errbox');
if (errbox)
errbox.parentNode.removeChild(errbox);
if (!ex)
return;
var msg = (ex + '').split('\n')[0];
var ln = ex.lineNumber;
var o = null;
if (ln) {
msg = "Line " + ln + ", " + msg;
var lns = js.split('\n');
if (ln < lns.length) {
o = document.createElement('span');
o.style.cssText = 'color:#ac2;font-size:.9em;font-family:scp;display:block';
o.textContent = lns[ln - 1];
}
}
errbox = document.createElement('div');
errbox.setAttribute('id', 'md_errbox');
errbox.style.cssText = 'position:absolute;top:0;left:0;padding:1em .5em;background:#2b2b2b;color:#fc5'
errbox.textContent = msg;
errbox.onclick = function () {
alert('' + ex.stack);
};
if (o) {
errbox.appendChild(o);
errbox.style.padding = '.25em .5em';
}
dom_nav.appendChild(errbox);
try {
console.trace();
}
catch (ex2) { }
}
function load_plug(md_text, plug_type) {
if (!md_opt.allow_plugins)
return md_text;
var find = '\n```copyparty_' + plug_type + '\n';
var ofs = md_text.indexOf(find);
if (ofs === -1)
return md_text;
var ofs2 = md_text.indexOf('\n```', ofs + 1);
if (ofs2 == -1)
return md_text;
var js = md_text.slice(ofs + find.length, ofs2 + 1);
var md = md_text.slice(0, ofs + 1) + md_text.slice(ofs2 + 4);
var old_plug = md_plug[plug_type];
if (!old_plug || old_plug[1] != js) {
js = 'const x = { ' + js + ' }; x;';
try {
var x = eval(js);
}
catch (ex) {
md_plug[plug_type] = null;
md_plug_err(ex, js);
return md;
}
if (x['ctor']) {
x['ctor']();
delete x['ctor'];
}
md_plug[plug_type] = [x, js];
}
return md;
}
function convert_markdown(md_text, dest_dom) { function convert_markdown(md_text, dest_dom) {
marked.setOptions({ md_text = md_text.replace(/\r/g, '');
md_plug_err(null);
md_text = load_plug(md_text, 'pre');
md_text = load_plug(md_text, 'post');
var marked_opts = {
//headerPrefix: 'h-', //headerPrefix: 'h-',
breaks: true, breaks: true,
gfm: true gfm: true
}); };
var md_html = marked(md_text);
var ext = md_plug['pre'];
if (ext)
Object.assign(marked_opts, ext[0]);
try {
var md_html = marked(md_text, marked_opts);
}
catch (ex) {
if (ext)
md_plug_err(ex, ext[1]);
throw ex;
}
var md_dom = new DOMParser().parseFromString(md_html, "text/html").body; var md_dom = new DOMParser().parseFromString(md_html, "text/html").body;
var nodes = md_dom.getElementsByTagName('a'); var nodes = md_dom.getElementsByTagName('a');
@@ -196,7 +299,7 @@ function convert_markdown(md_text, dest_dom) {
} }
// separate <code> for each line in <pre> // separate <code> for each line in <pre>
var nodes = md_dom.getElementsByTagName('pre'); nodes = md_dom.getElementsByTagName('pre');
for (var a = nodes.length - 1; a >= 0; a--) { for (var a = nodes.length - 1; a >= 0; a--) {
var el = nodes[a]; var el = nodes[a];
@@ -209,7 +312,7 @@ function convert_markdown(md_text, dest_dom) {
continue; continue;
var nline = parseInt(el.getAttribute('data-ln')) + 1; var nline = parseInt(el.getAttribute('data-ln')) + 1;
var lines = el.innerHTML.replace(/\r?\n<\/code>$/i, '</code>').split(/\r?\n/g); var lines = el.innerHTML.replace(/\n<\/code>$/i, '</code>').split(/\n/g);
for (var b = 0; b < lines.length - 1; b++) for (var b = 0; b < lines.length - 1; b++)
lines[b] += '</code>\n<code data-ln="' + (nline + b) + '">'; lines[b] += '</code>\n<code data-ln="' + (nline + b) + '">';
@@ -242,12 +345,29 @@ function convert_markdown(md_text, dest_dom) {
el.innerHTML = '<a href="#' + id + '">' + el.innerHTML + '</a>'; el.innerHTML = '<a href="#' + id + '">' + el.innerHTML + '</a>';
} }
ext = md_plug['post'];
if (ext && ext[0].render)
try {
ext[0].render(md_dom);
}
catch (ex) {
md_plug_err(ex, ext[1]);
}
copydom(md_dom, dest_dom, 0); copydom(md_dom, dest_dom, 0);
if (ext && ext[0].render2)
try {
ext[0].render2(dest_dom);
}
catch (ex) {
md_plug_err(ex, ext[1]);
}
} }
function init_toc() { function init_toc() {
var loader = document.getElementById('ml'); var loader = ebi('ml');
loader.parentNode.removeChild(loader); loader.parentNode.removeChild(loader);
var anchors = []; // list of toc entries, complex objects var anchors = []; // list of toc entries, complex objects
@@ -281,7 +401,12 @@ function init_toc() {
elm.childNodes[0].setAttribute('ctr', ctr.slice(0, lv).join('.')); elm.childNodes[0].setAttribute('ctr', ctr.slice(0, lv).join('.'));
html.push('<li>' + elm.innerHTML + '</li>'); var elm2 = elm.cloneNode(true);
elm2.childNodes[0].textContent = elm.textContent;
while (elm2.childNodes.length > 1)
elm2.removeChild(elm2.childNodes[1]);
html.push('<li>' + elm2.innerHTML + '</li>');
if (anchor != null) if (anchor != null)
anchors.push(anchor); anchors.push(anchor);

View File

@@ -77,32 +77,52 @@ html.dark #mt {
background: #f97; background: #f97;
border-radius: .15em; border-radius: .15em;
} }
html.dark #save.force-save {
color: #fca;
background: #720;
}
#save.disabled { #save.disabled {
opacity: .4; opacity: .4;
} }
#helpbox,
#toast {
background: #f7f7f7;
border-radius: .4em;
z-index: 9001;
}
#helpbox { #helpbox {
display: none; display: none;
position: fixed; position: fixed;
background: #f7f7f7;
box-shadow: 0 .5em 2em #777;
border-radius: .4em;
padding: 2em; padding: 2em;
top: 4em; top: 4em;
overflow-y: auto; overflow-y: auto;
box-shadow: 0 .5em 2em #777;
height: calc(100% - 12em); height: calc(100% - 12em);
left: calc(50% - 15em); left: calc(50% - 15em);
right: 0; right: 0;
width: 30em; width: 30em;
z-index: 9001;
} }
#helpclose { #helpclose {
display: block; display: block;
} }
html.dark #helpbox { html.dark #helpbox {
background: #222;
box-shadow: 0 .5em 2em #444; box-shadow: 0 .5em 2em #444;
}
html.dark #helpbox,
html.dark #toast {
background: #222;
border: 1px solid #079; border: 1px solid #079;
border-width: 1px 0; border-width: 1px 0;
} }
#toast {
font-weight: bold;
text-align: center;
padding: .6em 0;
position: fixed;
z-index: 9001;
top: 30%;
transition: opacity 0.2s ease-in-out;
opacity: 1;
}
# mt {opacity: .5;top:1px} # mt {opacity: .5;top:1px}

View File

@@ -1,3 +1,6 @@
"use strict";
// server state // server state
var server_md = dom_src.value; var server_md = dom_src.value;
@@ -8,15 +11,15 @@ var js_uni_whitelist = eval('\'' + esc_uni_whitelist + '\'');
// dom nodes // dom nodes
var dom_swrap = document.getElementById('mtw'); var dom_swrap = ebi('mtw');
var dom_sbs = document.getElementById('sbs'); var dom_sbs = ebi('sbs');
var dom_nsbs = document.getElementById('nsbs'); var dom_nsbs = ebi('nsbs');
var dom_tbox = document.getElementById('toolsbox'); var dom_tbox = ebi('toolsbox');
var dom_ref = (function () { var dom_ref = (function () {
var d = document.createElement('div'); var d = document.createElement('div');
d.setAttribute('id', 'mtr'); d.setAttribute('id', 'mtr');
dom_swrap.appendChild(d); dom_swrap.appendChild(d);
d = document.getElementById('mtr'); d = ebi('mtr');
// hide behind the textarea (offsetTop is not computed if display:none) // hide behind the textarea (offsetTop is not computed if display:none)
dom_src.style.zIndex = '4'; dom_src.style.zIndex = '4';
d.style.zIndex = '3'; d.style.zIndex = '3';
@@ -105,7 +108,7 @@ var draw_md = (function () {
map_src = genmap(dom_ref, map_src); map_src = genmap(dom_ref, map_src);
map_pre = genmap(dom_pre, map_pre); map_pre = genmap(dom_pre, map_pre);
cls(document.getElementById('save'), 'disabled', src == server_md); cls(ebi('save'), 'disabled', src == server_md);
var t1 = new Date().getTime(); var t1 = new Date().getTime();
delay = t1 - t0 > 100 ? 25 : 1; delay = t1 - t0 > 100 ? 25 : 1;
@@ -141,7 +144,7 @@ redraw = (function () {
onresize(); onresize();
} }
function modetoggle() { function modetoggle() {
mode = dom_nsbs.innerHTML; var mode = dom_nsbs.innerHTML;
dom_nsbs.innerHTML = mode == 'editor' ? 'preview' : 'editor'; dom_nsbs.innerHTML = mode == 'editor' ? 'preview' : 'editor';
mode += ' single'; mode += ' single';
dom_wrap.setAttribute('class', mode); dom_wrap.setAttribute('class', mode);
@@ -177,7 +180,7 @@ redraw = (function () {
y += src.clientHeight / 2; y += src.clientHeight / 2;
var sy1 = -1, sy2 = -1, dy1 = -1, dy2 = -1; var sy1 = -1, sy2 = -1, dy1 = -1, dy2 = -1;
for (var a = 1; a < nlines + 1; a++) { for (var a = 1; a < nlines + 1; a++) {
if (srcmap[a] === null || dstmap[a] === null) if (srcmap[a] == null || dstmap[a] == null)
continue; continue;
if (srcmap[a] > y) { if (srcmap[a] > y) {
@@ -220,14 +223,108 @@ redraw = (function () {
})(); })();
// modification checker
function Modpoll() {
this.skip_one = true;
this.disabled = false;
this.periodic = function () {
var that = this;
setTimeout(function () {
that.periodic();
}, 1000 * md_opt.modpoll_freq);
var skip = null;
if (ebi('toast'))
skip = 'toast';
else if (this.skip_one)
skip = 'saved';
else if (this.disabled)
skip = 'disabled';
if (skip) {
console.log('modpoll skip, ' + skip);
this.skip_one = false;
return;
}
console.log('modpoll...');
var url = (document.location + '').split('?')[0] + '?raw&_=' + new Date().getTime();
var xhr = new XMLHttpRequest();
xhr.modpoll = this;
xhr.open('GET', url, true);
xhr.responseType = 'text';
xhr.onreadystatechange = this.cb;
xhr.send();
}
this.cb = function () {
if (this.modpoll.disabled || this.modpoll.skip_one) {
console.log('modpoll abort');
return;
}
if (this.readyState != XMLHttpRequest.DONE)
return;
if (this.status !== 200) {
console.log('modpoll err ' + this.status + ": " + this.responseText);
return;
}
if (!this.responseText)
return;
var server_ref = server_md.replace(/\r/g, '');
var server_now = this.responseText.replace(/\r/g, '');
if (server_ref != server_now) {
console.log("modpoll diff |" + server_ref.length + "|, |" + server_now.length + "|");
this.modpoll.disabled = true;
var msg = [
"The document has changed on the server.<br />" +
"The changes will NOT be loaded into your editor automatically.",
"Press F5 or CTRL-R to refresh the page,<br />" +
"replacing your document with the server copy.",
"You can click this message to ignore and contnue."
];
return toast(false, "box-shadow:0 1em 2em rgba(64,64,64,0.8);font-weight:normal",
36, "<p>" + msg.join('</p>\n<p>') + '</p>');
}
console.log('modpoll eq');
}
if (md_opt.modpoll_freq > 0)
this.periodic();
return this;
}
var modpoll = new Modpoll();
window.onbeforeunload = function (e) {
if ((ebi("save").getAttribute('class') + '').indexOf('disabled') >= 0)
return; //nice (todo)
e.preventDefault(); //ff
e.returnValue = ''; //chrome
};
// save handler // save handler
function save(e) { function save(e) {
if (e) e.preventDefault(); if (e) e.preventDefault();
var save_btn = document.getElementById("save"), var save_btn = ebi("save"),
save_cls = save_btn.getAttribute('class') + ''; save_cls = save_btn.getAttribute('class') + '';
if (save_cls.indexOf('disabled') >= 0) { if (save_cls.indexOf('disabled') >= 0) {
toast('font-size:2em;color:#fc6;width:9em;', 'no changes'); toast(true, ";font-size:2em;color:#c90", 9, "no changes");
return; return;
} }
@@ -251,6 +348,8 @@ function save(e) {
xhr.onreadystatechange = save_cb; xhr.onreadystatechange = save_cb;
xhr.btn = save_btn; xhr.btn = save_btn;
xhr.txt = txt; xhr.txt = txt;
modpoll.skip_one = true; // skip one iteration while we save
xhr.send(fd); xhr.send(fd);
} }
@@ -344,23 +443,44 @@ function savechk_cb() {
last_modified = this.lastmod; last_modified = this.lastmod;
server_md = this.txt; server_md = this.txt;
draw_md(); draw_md();
toast('font-size:6em;font-family:serif;color:#cf6;width:4em;', toast(true, ";font-size:6em;font-family:serif;color:#9b4", 4,
'OK✔<span style="font-size:.2em;color:#999;position:absolute">' + this.ntry + '</span>'); 'OK✔<span style="font-size:.2em;color:#999;position:absolute">' + this.ntry + '</span>');
modpoll.disabled = false;
} }
function toast(style, msg) { function toast(autoclose, style, width, msg) {
var ok = document.createElement('div'); var ok = ebi("toast");
style += 'font-weight:bold;background:#444;border-radius:.3em;padding:.6em 0;position:fixed;top:30%;left:calc(50% - 2em);text-align:center;z-index:9001;transition:opacity 0.2s ease-in-out;opacity:1'; if (ok)
ok.parentNode.removeChild(ok);
style = "width:" + width + "em;left:calc(50% - " + (width / 2) + "em);" + style;
ok = document.createElement('div');
ok.setAttribute('id', 'toast');
ok.setAttribute('style', style); ok.setAttribute('style', style);
ok.innerHTML = msg; ok.innerHTML = msg;
var parent = document.getElementById('m'); var parent = ebi('m');
document.documentElement.appendChild(ok); document.documentElement.appendChild(ok);
setTimeout(function () {
ok.style.opacity = 0; var hide = function (delay) {
}, 500); delay = delay || 0;
setTimeout(function () {
ok.parentNode.removeChild(ok); setTimeout(function () {
}, 750); ok.style.opacity = 0;
}, delay);
setTimeout(function () {
if (ok.parentNode)
ok.parentNode.removeChild(ok);
}, delay + 250);
}
ok.onclick = function () {
hide(0);
};
if (autoclose)
hide(500);
} }
@@ -540,6 +660,10 @@ function md_backspace() {
if (/^\s*$/.test(left)) if (/^\s*$/.test(left))
return true; return true;
// same if selection
if (o0 != dom_src.selectionEnd)
return true;
// same if line is all-whitespace or non-markup // same if line is all-whitespace or non-markup
var v = m[0].replace(/[^ ]/g, " "); var v = m[0].replace(/[^ ]/g, " ");
if (v === m[0] || v.length !== left.length) if (v === m[0] || v.length !== left.length)
@@ -602,7 +726,7 @@ function fmt_table(e) {
//o0 = txt.lastIndexOf('\n\n', ofs), //o0 = txt.lastIndexOf('\n\n', ofs),
//o1 = txt.indexOf('\n\n', ofs); //o1 = txt.indexOf('\n\n', ofs);
o0 = reLastIndexOf(txt, /\n\s*\n/m, ofs), o0 = reLastIndexOf(txt, /\n\s*\n/m, ofs),
o1 = txt.slice(ofs).search(/\n\s*\n/m); o1 = txt.slice(ofs).search(/\n\s*\n|\n\s*$/m);
// note \s contains \n but its fine // note \s contains \n but its fine
if (o0 < 0) if (o0 < 0)
@@ -623,12 +747,21 @@ function fmt_table(e) {
lpipe = tab[1].indexOf('|') < tab[1].indexOf('-'), lpipe = tab[1].indexOf('|') < tab[1].indexOf('-'),
rpipe = tab[1].lastIndexOf('|') > tab[1].lastIndexOf('-'), rpipe = tab[1].lastIndexOf('|') > tab[1].lastIndexOf('-'),
re_lpipe = lpipe ? /^\s*\|\s*/ : /^\s*/, re_lpipe = lpipe ? /^\s*\|\s*/ : /^\s*/,
re_rpipe = rpipe ? /\s*\|\s*$/ : /\s*$/; re_rpipe = rpipe ? /\s*\|\s*$/ : /\s*$/,
ncols;
// the second row defines the table,
// need to process that first
var tmp = tab[0];
tab[0] = tab[1];
tab[1] = tmp;
for (var a = 0; a < tab.length; a++) { for (var a = 0; a < tab.length; a++) {
var row_name = (a == 1) ? 'header' : 'row#' + (a + 1);
var ind2 = tab[a].match(re_ind)[0]; var ind2 = tab[a].match(re_ind)[0];
if (ind != ind2 && a > 0) // the table can be a list entry or something, ignore [0] if (ind != ind2 && a != 1) // the table can be a list entry or something, ignore [0]
return alert(err + 'indentation mismatch on row 2 and ' + (a + 1) + ',\n' + tab[a]); return alert(err + 'indentation mismatch on row#2 and ' + row_name + ',\n' + tab[a]);
var t = tab[a].slice(ind.length); var t = tab[a].slice(ind.length);
t = t.replace(re_lpipe, ""); t = t.replace(re_lpipe, "");
@@ -637,17 +770,25 @@ function fmt_table(e) {
if (a == 0) if (a == 0)
ncols = tab[a].length; ncols = tab[a].length;
else if (ncols < tab[a].length)
return alert(err + 'num.columns(' + row_name + ') exceeding row#2; ' + ncols + ' < ' + tab[a].length);
if (ncols != tab[a].length) // if row has less columns than row2, fill them in
return alert(err + 'num.columns mismatch on row 2 and ' + (a + 1) + '; ' + ncols + ' != ' + tab[a].length); while (tab[a].length < ncols)
tab[a].push('');
} }
// aight now swap em back
tmp = tab[0];
tab[0] = tab[1];
tab[1] = tmp;
var re_align = /^ *(:?)-+(:?) *$/; var re_align = /^ *(:?)-+(:?) *$/;
var align = []; var align = [];
for (var col = 0; col < tab[1].length; col++) { for (var col = 0; col < tab[1].length; col++) {
var m = tab[1][col].match(re_align); var m = tab[1][col].match(re_align);
if (!m) if (!m)
return alert(err + 'invalid column specification, row 2, col ' + (col + 1) + ', [' + tab[1][col] + ']'); return alert(err + 'invalid column specification, row#2, col ' + (col + 1) + ', [' + tab[1][col] + ']');
if (m[2]) { if (m[2]) {
if (m[1]) if (m[1])
@@ -664,7 +805,8 @@ function fmt_table(e) {
for (var col = 0; col < ncols; col++) { for (var col = 0; col < ncols; col++) {
var max = 0; var max = 0;
for (var row = 0; row < tab.length; row++) for (var row = 0; row < tab.length; row++)
max = Math.max(max, tab[row][col].length); if (row != 1)
max = Math.max(max, tab[row][col].length);
var s = ''; var s = '';
for (var n = 0; n < max; n++) for (var n = 0; n < max; n++)
@@ -731,9 +873,8 @@ function mark_uni(e) {
dom_tbox.setAttribute('class', ''); dom_tbox.setAttribute('class', '');
var txt = dom_src.value, var txt = dom_src.value,
ptn = new RegExp('([^' + js_uni_whitelist + ']+)', 'g'); ptn = new RegExp('([^' + js_uni_whitelist + ']+)', 'g'),
mod = txt.replace(/\r/g, "").replace(ptn, "\u2588\u2770$1\u2771");
mod = txt.replace(/\r/g, "").replace(ptn, "\u2588\u2770$1\u2771");
if (txt == mod) { if (txt == mod) {
alert('no results; no modifications were made'); alert('no results; no modifications were made');
@@ -769,7 +910,12 @@ function iter_uni(e) {
// configure whitelist // configure whitelist
function cfg_uni(e) { function cfg_uni(e) {
if (e) e.preventDefault(); if (e) e.preventDefault();
esc_uni_whitelist = prompt("unicode whitelist", esc_uni_whitelist);
var reply = prompt("unicode whitelist", esc_uni_whitelist);
if (reply === null)
return;
esc_uni_whitelist = reply;
js_uni_whitelist = eval('\'' + esc_uni_whitelist + '\''); js_uni_whitelist = eval('\'' + esc_uni_whitelist + '\'');
} }
@@ -786,7 +932,7 @@ function cfg_uni(e) {
return false; return false;
} }
if (ev.code == "Escape" || kc == 27) { if (ev.code == "Escape" || kc == 27) {
var d = document.getElementById('helpclose'); var d = ebi('helpclose');
if (d) if (d)
d.click(); d.click();
} }
@@ -843,22 +989,22 @@ function cfg_uni(e) {
} }
} }
document.onkeydown = keydown; document.onkeydown = keydown;
document.getElementById('save').onclick = save; ebi('save').onclick = save;
})(); })();
document.getElementById('tools').onclick = function (e) { ebi('tools').onclick = function (e) {
if (e) e.preventDefault(); if (e) e.preventDefault();
var is_open = dom_tbox.getAttribute('class') != 'open'; var is_open = dom_tbox.getAttribute('class') != 'open';
dom_tbox.setAttribute('class', is_open ? 'open' : ''); dom_tbox.setAttribute('class', is_open ? 'open' : '');
}; };
document.getElementById('help').onclick = function (e) { ebi('help').onclick = function (e) {
if (e) e.preventDefault(); if (e) e.preventDefault();
dom_tbox.setAttribute('class', ''); dom_tbox.setAttribute('class', '');
var dom = document.getElementById('helpbox'); var dom = ebi('helpbox');
var dtxt = dom.getElementsByTagName('textarea'); var dtxt = dom.getElementsByTagName('textarea');
if (dtxt.length > 0) { if (dtxt.length > 0) {
convert_markdown(dtxt[0].value, dom); convert_markdown(dtxt[0].value, dom);
@@ -866,16 +1012,16 @@ document.getElementById('help').onclick = function (e) {
} }
dom.style.display = 'block'; dom.style.display = 'block';
document.getElementById('helpclose').onclick = function () { ebi('helpclose').onclick = function () {
dom.style.display = 'none'; dom.style.display = 'none';
}; };
}; };
document.getElementById('fmt_table').onclick = fmt_table; ebi('fmt_table').onclick = fmt_table;
document.getElementById('mark_uni').onclick = mark_uni; ebi('mark_uni').onclick = mark_uni;
document.getElementById('iter_uni').onclick = iter_uni; ebi('iter_uni').onclick = iter_uni;
document.getElementById('cfg_uni').onclick = cfg_uni; ebi('cfg_uni').onclick = cfg_uni;
// blame steen // blame steen
@@ -983,13 +1129,12 @@ action_stack = (function () {
ref = newtxt; ref = newtxt;
dbg('undos(%d) redos(%d)', hist.un.length, hist.re.length); dbg('undos(%d) redos(%d)', hist.un.length, hist.re.length);
if (hist.un.length > 0) if (hist.un.length > 0)
dbg(static(hist.un.slice(-1)[0])); dbg(statify(hist.un.slice(-1)[0]));
if (hist.re.length > 0) if (hist.re.length > 0)
dbg(static(hist.re.slice(-1)[0])); dbg(statify(hist.re.slice(-1)[0]));
} }
return { return {
push: push,
undo: undo, undo: undo,
redo: redo, redo: redo,
push: schedule_push, push: schedule_push,
@@ -999,7 +1144,7 @@ action_stack = (function () {
})(); })();
/* /*
document.getElementById('help').onclick = function () { ebi('help').onclick = function () {
var c1 = getComputedStyle(dom_src).cssText.split(';'); var c1 = getComputedStyle(dom_src).cssText.split(';');
var c2 = getComputedStyle(dom_ref).cssText.split(';'); var c2 = getComputedStyle(dom_ref).cssText.split(';');
var max = Math.min(c1.length, c2.length); var max = Math.min(c1.length, c2.length);

View File

@@ -22,8 +22,12 @@
</div> </div>
<script> <script>
var link_md_as_html = false; // TODO (does nothing)
var last_modified = {{ lastmod }}; var last_modified = {{ lastmod }};
var md_opt = {
link_md_as_html: false,
allow_plugins: {{ md_plug }},
modpoll_freq: {{ md_chk_rate }}
};
var lightswitch = (function () { var lightswitch = (function () {
var fun = function () { var fun = function () {
@@ -39,6 +43,7 @@ var lightswitch = (function () {
})(); })();
</script> </script>
<script src="/.cpr/util.js"></script>
<script src="/.cpr/deps/easymde.js"></script> <script src="/.cpr/deps/easymde.js"></script>
<script src="/.cpr/mde.js"></script> <script src="/.cpr/mde.js"></script>
</body></html> </body></html>

View File

@@ -1,7 +1,9 @@
var dom_wrap = document.getElementById('mw'); "use strict";
var dom_nav = document.getElementById('mn');
var dom_doc = document.getElementById('m'); var dom_wrap = ebi('mw');
var dom_md = document.getElementById('mt'); var dom_nav = ebi('mn');
var dom_doc = ebi('m');
var dom_md = ebi('mt');
(function () { (function () {
var n = document.location + ''; var n = document.location + '';
@@ -63,7 +65,7 @@ var mde = (function () {
mde.codemirror.on("change", function () { mde.codemirror.on("change", function () {
md_changed(mde); md_changed(mde);
}); });
var loader = document.getElementById('ml'); var loader = ebi('ml');
loader.parentNode.removeChild(loader); loader.parentNode.removeChild(loader);
return mde; return mde;
})(); })();
@@ -213,7 +215,7 @@ function save_chk() {
var ok = document.createElement('div'); var ok = document.createElement('div');
ok.setAttribute('style', 'font-size:6em;font-family:serif;font-weight:bold;color:#cf6;background:#444;border-radius:.3em;padding:.6em 0;position:fixed;top:30%;left:calc(50% - 2em);width:4em;text-align:center;z-index:9001;transition:opacity 0.2s ease-in-out;opacity:1'); ok.setAttribute('style', 'font-size:6em;font-family:serif;font-weight:bold;color:#cf6;background:#444;border-radius:.3em;padding:.6em 0;position:fixed;top:30%;left:calc(50% - 2em);width:4em;text-align:center;z-index:9001;transition:opacity 0.2s ease-in-out;opacity:1');
ok.innerHTML = 'OK✔'; ok.innerHTML = 'OK✔';
var parent = document.getElementById('m'); var parent = ebi('m');
document.documentElement.appendChild(ok); document.documentElement.appendChild(ok);
setTimeout(function () { setTimeout(function () {
ok.style.opacity = 0; ok.style.opacity = 0;

View File

@@ -1,61 +1,6 @@
"use strict"; "use strict";
// error handler for mobile devices window.onerror = vis_exh;
function hcroak(msg) {
document.body.innerHTML = msg;
window.onerror = undefined;
throw 'fatal_err';
}
function croak(msg) {
document.body.textContent = msg;
window.onerror = undefined;
throw msg;
}
function esc(txt) {
return txt.replace(/[&"<>]/g, function (c) {
return {
'&': '&amp;',
'"': '&quot;',
'<': '&lt;',
'>': '&gt;'
}[c];
});
}
window.onerror = function (msg, url, lineNo, columnNo, error) {
window.onerror = undefined;
var html = ['<h1>you hit a bug!</h1><p>please screenshot this error and send me a copy arigathanks gozaimuch (ed/irc.rizon.net or ed#2644)</p><p>',
esc(String(msg)), '</p><p>', esc(url + ' @' + lineNo + ':' + columnNo), '</p>'];
if (error) {
var find = ['desc', 'stack', 'trace'];
for (var a = 0; a < find.length; a++)
if (String(error[find[a]]) !== 'undefined')
html.push('<h2>' + find[a] + '</h2>' +
esc(String(error[find[a]])).replace(/\n/g, '<br />\n'));
}
document.body.style.fontSize = '0.8em';
document.body.style.padding = '0 1em 1em 1em';
hcroak(html.join('\n'));
};
// https://stackoverflow.com/a/950146
function import_js(url, cb) {
var head = document.head || document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = url;
script.onreadystatechange = cb;
script.onload = cb;
head.appendChild(script);
}
function o(id) {
return document.getElementById(id);
}
(function () { (function () {
@@ -88,12 +33,12 @@ function goto(dest) {
for (var a = obj.length - 1; a >= 0; a--) for (var a = obj.length - 1; a >= 0; a--)
obj[a].classList.remove('act'); obj[a].classList.remove('act');
var obj = document.querySelectorAll('#ops>a'); obj = document.querySelectorAll('#ops>a');
for (var a = obj.length - 1; a >= 0; a--) for (var a = obj.length - 1; a >= 0; a--)
obj[a].classList.remove('act'); obj[a].classList.remove('act');
if (dest) { if (dest) {
document.getElementById('op_' + dest).classList.add('act'); ebi('op_' + dest).classList.add('act');
document.querySelector('#ops>a[data-dest=' + dest + ']').classList.add('act'); document.querySelector('#ops>a[data-dest=' + dest + ']').classList.add('act');
var fn = window['goto_' + dest]; var fn = window['goto_' + dest];
@@ -121,7 +66,7 @@ function goto_up2k() {
if (op !== null && op !== '.') if (op !== null && op !== '.')
goto(op); goto(op);
} }
document.getElementById('ops').style.display = 'block'; ebi('ops').style.display = 'block';
})(); })();
@@ -150,21 +95,21 @@ function up2k_init(have_crypto) {
// show modal message // show modal message
function showmodal(msg) { function showmodal(msg) {
o('u2notbtn').innerHTML = msg; ebi('u2notbtn').innerHTML = msg;
o('u2btn').style.display = 'none'; ebi('u2btn').style.display = 'none';
o('u2notbtn').style.display = 'block'; ebi('u2notbtn').style.display = 'block';
o('u2conf').style.opacity = '0.5'; ebi('u2conf').style.opacity = '0.5';
} }
// hide modal message // hide modal message
function unmodal() { function unmodal() {
o('u2notbtn').style.display = 'none'; ebi('u2notbtn').style.display = 'none';
o('u2btn').style.display = 'block'; ebi('u2btn').style.display = 'block';
o('u2conf').style.opacity = '1'; ebi('u2conf').style.opacity = '1';
o('u2notbtn').innerHTML = ''; ebi('u2notbtn').innerHTML = '';
} }
var post_url = o('op_bup').getElementsByTagName('form')[0].getAttribute('action'); var post_url = ebi('op_bup').getElementsByTagName('form')[0].getAttribute('action');
if (post_url && post_url.charAt(post_url.length - 1) !== '/') if (post_url && post_url.charAt(post_url.length - 1) !== '/')
post_url += '/'; post_url += '/';
@@ -181,25 +126,25 @@ function up2k_init(have_crypto) {
import_js('/.cpr/deps/sha512.js', unmodal); import_js('/.cpr/deps/sha512.js', unmodal);
if (is_https) if (is_https)
o('u2foot').innerHTML = shame + ' so <em>this</em> uploader will do like 500kB/s at best'; ebi('u2foot').innerHTML = shame + ' so <em>this</em> uploader will do like 500kB/s at best';
else else
o('u2foot').innerHTML = 'seems like ' + shame + ' so do that if you want more performance'; ebi('u2foot').innerHTML = 'seems like ' + shame + ' so do that if you want more performance';
} }
}; }
// show uploader if the user only has write-access // show uploader if the user only has write-access
if (!o('files')) if (!ebi('files'))
goto('up2k'); goto('up2k');
// shows or clears an error message in the basic uploader ui // shows or clears an error message in the basic uploader ui
function setmsg(msg) { function setmsg(msg) {
if (msg !== undefined) { if (msg !== undefined) {
o('u2err').setAttribute('class', 'err'); ebi('u2err').setAttribute('class', 'err');
o('u2err').innerHTML = msg; ebi('u2err').innerHTML = msg;
} }
else { else {
o('u2err').setAttribute('class', ''); ebi('u2err').setAttribute('class', '');
o('u2err').innerHTML = ''; ebi('u2err').innerHTML = '';
} }
} }
@@ -210,7 +155,7 @@ function up2k_init(have_crypto) {
} }
// handle user intent to use the basic uploader instead // handle user intent to use the basic uploader instead
o('u2nope').onclick = function (e) { ebi('u2nope').onclick = function (e) {
e.preventDefault(); e.preventDefault();
setmsg(''); setmsg('');
goto('bup'); goto('bup');
@@ -229,9 +174,9 @@ function up2k_init(have_crypto) {
function cfg_get(name) { function cfg_get(name) {
var val = localStorage.getItem(name); var val = localStorage.getItem(name);
if (val === null) if (val === null)
return parseInt(o(name).value); return parseInt(ebi(name).value);
o(name).value = val; ebi(name).value = val;
return val; return val;
} }
@@ -242,7 +187,7 @@ function up2k_init(have_crypto) {
else else
val = (val == '1'); val = (val == '1');
o(name).checked = val; ebi(name).checked = val;
return val; return val;
} }
@@ -250,7 +195,7 @@ function up2k_init(have_crypto) {
localStorage.setItem( localStorage.setItem(
name, val ? '1' : '0'); name, val ? '1' : '0');
o(name).checked = val; ebi(name).checked = val;
return val; return val;
} }
@@ -284,9 +229,9 @@ function up2k_init(have_crypto) {
return un2k("this is the basic uploader; up2k needs at least<br />chrome 21 // firefox 13 // edge 12 // opera 12 // safari 5.1"); return un2k("this is the basic uploader; up2k needs at least<br />chrome 21 // firefox 13 // edge 12 // opera 12 // safari 5.1");
function nav() { function nav() {
o('file' + fdom_ctr).click(); ebi('file' + fdom_ctr).click();
} }
o('u2btn').addEventListener('click', nav, false); ebi('u2btn').addEventListener('click', nav, false);
function ondrag(ev) { function ondrag(ev) {
ev.stopPropagation(); ev.stopPropagation();
@@ -294,8 +239,8 @@ function up2k_init(have_crypto) {
ev.dataTransfer.dropEffect = 'copy'; ev.dataTransfer.dropEffect = 'copy';
ev.dataTransfer.effectAllowed = 'copy'; ev.dataTransfer.effectAllowed = 'copy';
} }
o('u2btn').addEventListener('dragover', ondrag, false); ebi('u2btn').addEventListener('dragover', ondrag, false);
o('u2btn').addEventListener('dragenter', ondrag, false); ebi('u2btn').addEventListener('dragenter', ondrag, false);
function gotfile(ev) { function gotfile(ev) {
ev.stopPropagation(); ev.stopPropagation();
@@ -357,7 +302,7 @@ function up2k_init(have_crypto) {
var tr = document.createElement('tr'); var tr = document.createElement('tr');
tr.innerHTML = '<td id="f{0}n"></td><td id="f{0}t">hashing</td><td id="f{0}p" class="prog"></td>'.format(st.files.length); tr.innerHTML = '<td id="f{0}n"></td><td id="f{0}t">hashing</td><td id="f{0}p" class="prog"></td>'.format(st.files.length);
tr.getElementsByTagName('td')[0].textContent = entry.name; tr.getElementsByTagName('td')[0].textContent = entry.name;
o('u2tab').appendChild(tr); ebi('u2tab').appendChild(tr);
st.files.push(entry); st.files.push(entry);
st.todo.hash.push(entry); st.todo.hash.push(entry);
@@ -374,14 +319,14 @@ function up2k_init(have_crypto) {
alert(msg); alert(msg);
} }
} }
o('u2btn').addEventListener('drop', gotfile, false); ebi('u2btn').addEventListener('drop', gotfile, false);
function more_one_file() { function more_one_file() {
fdom_ctr++; fdom_ctr++;
var elm = document.createElement('div') var elm = document.createElement('div')
elm.innerHTML = '<input id="file{0}" type="file" name="file{0}[]" multiple="multiple" />'.format(fdom_ctr); elm.innerHTML = '<input id="file{0}" type="file" name="file{0}[]" multiple="multiple" />'.format(fdom_ctr);
o('u2form').appendChild(elm); ebi('u2form').appendChild(elm);
o('file' + fdom_ctr).addEventListener('change', gotfile, false); ebi('file' + fdom_ctr).addEventListener('change', gotfile, false);
} }
more_one_file(); more_one_file();
@@ -451,17 +396,6 @@ function up2k_init(have_crypto) {
/// hashing /// hashing
// //
// https://gist.github.com/jonleighton/958841
function buf2b64_maybe_fucky(buffer) {
var ret = '';
var view = new DataView(buffer);
for (var i = 0; i < view.byteLength; i++) {
ret += String.fromCharCode(view.getUint8(i));
}
return window.btoa(ret).replace(
/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
// https://gist.github.com/jonleighton/958841 // https://gist.github.com/jonleighton/958841
function buf2b64(arrayBuffer) { function buf2b64(arrayBuffer) {
var base64 = ''; var base64 = '';
@@ -502,20 +436,6 @@ function up2k_init(have_crypto) {
return base64; return base64;
} }
// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest
function buf2hex(buffer) {
var hexCodes = [];
var view = new DataView(buffer);
for (var i = 0; i < view.byteLength; i += 4) {
var value = view.getUint32(i) // 4 bytes per iter
var stringValue = value.toString(16) // doesn't pad
var padding = '00000000'
var paddedValue = (padding + stringValue).slice(-padding.length)
hexCodes.push(paddedValue);
}
return hexCodes.join("");
}
function get_chunksize(filesize) { function get_chunksize(filesize) {
var chunksize = 1024 * 1024; var chunksize = 1024 * 1024;
var stepsize = 512 * 1024; var stepsize = 512 * 1024;
@@ -602,7 +522,7 @@ function up2k_init(have_crypto) {
pb_html += '<div id="f{0}p{1}" style="width:{2}%"><div></div></div>'.format( pb_html += '<div id="f{0}p{1}" style="width:{2}%"><div></div></div>'.format(
t.n, a, pb_perc); t.n, a, pb_perc);
o('f{0}p'.format(t.n)).innerHTML = pb_html; ebi('f{0}p'.format(t.n)).innerHTML = pb_html;
var reader = new FileReader(); var reader = new FileReader();
@@ -677,7 +597,7 @@ function up2k_init(have_crypto) {
alert('{0} ms, {1} MB/s\n'.format(t.t2 - t.t1, spd.toFixed(3)) + t.hash.join('\n')); alert('{0} ms, {1} MB/s\n'.format(t.t2 - t.t1, spd.toFixed(3)) + t.hash.join('\n'));
} }
o('f{0}t'.format(t.n)).innerHTML = 'connecting'; ebi('f{0}t'.format(t.n)).innerHTML = 'connecting';
st.busy.hash.splice(st.busy.hash.indexOf(t), 1); st.busy.hash.splice(st.busy.hash.indexOf(t), 1);
st.todo.handshake.push(t); st.todo.handshake.push(t);
}; };
@@ -706,7 +626,7 @@ function up2k_init(have_crypto) {
if (response.name !== t.name) { if (response.name !== t.name) {
// file exists; server renamed us // file exists; server renamed us
t.name = response.name; t.name = response.name;
o('f{0}n'.format(t.n)).textContent = t.name; ebi('f{0}n'.format(t.n)).textContent = t.name;
} }
t.postlist = []; t.postlist = [];
@@ -736,23 +656,37 @@ function up2k_init(have_crypto) {
msg = 'uploading'; msg = 'uploading';
done = false; done = false;
} }
o('f{0}t'.format(t.n)).innerHTML = msg; ebi('f{0}t'.format(t.n)).innerHTML = msg;
st.busy.handshake.splice(st.busy.handshake.indexOf(t), 1); st.busy.handshake.splice(st.busy.handshake.indexOf(t), 1);
if (done) { if (done) {
var spd1 = (t.size / ((t.t2 - t.t1) / 1000.)) / (1024 * 1024.); var spd1 = (t.size / ((t.t2 - t.t1) / 1000.)) / (1024 * 1024.);
var spd2 = (t.size / ((t.t3 - t.t2) / 1000.)) / (1024 * 1024.); var spd2 = (t.size / ((t.t3 - t.t2) / 1000.)) / (1024 * 1024.);
o('f{0}p'.format(t.n)).innerHTML = 'hash {0}, up {1} MB/s'.format( ebi('f{0}p'.format(t.n)).innerHTML = 'hash {0}, up {1} MB/s'.format(
spd1.toFixed(2), spd2.toFixed(2)); spd1.toFixed(2), spd2.toFixed(2));
} }
tasker(); tasker();
} }
else else {
var err = "";
var rsp = (xhr.responseText + '');
if (rsp.indexOf('partial upload exists') !== -1) {
err = rsp.slice(5);
}
if (err != "") {
ebi('f{0}t'.format(t.n)).innerHTML = "ERROR";
ebi('f{0}p'.format(t.n)).innerHTML = err;
st.busy.handshake.splice(st.busy.handshake.indexOf(t), 1);
tasker();
return;
}
alert("server broke (error {0}):\n\"{1}\"\n".format( alert("server broke (error {0}):\n\"{1}\"\n".format(
xhr.status, xhr.status,
(xhr.response && xhr.response.err) || (xhr.response && xhr.response.err) ||
(xhr.responseText && xhr.responseText) || (xhr.responseText && xhr.responseText) ||
"no further information")); "no further information"));
}
}; };
xhr.open('POST', post_url + 'handshake.php', true); xhr.open('POST', post_url + 'handshake.php', true);
xhr.responseType = 'text'; xhr.responseType = 'text';
@@ -803,7 +737,7 @@ function up2k_init(have_crypto) {
t.postlist.splice(t.postlist.indexOf(npart), 1); t.postlist.splice(t.postlist.indexOf(npart), 1);
if (t.postlist.length == 0) { if (t.postlist.length == 0) {
t.t3 = new Date().getTime(); t.t3 = new Date().getTime();
o('f{0}t'.format(t.n)).innerHTML = 'verifying'; ebi('f{0}t'.format(t.n)).innerHTML = 'verifying';
st.todo.handshake.push(t); st.todo.handshake.push(t);
} }
tasker(); tasker();
@@ -834,7 +768,7 @@ function up2k_init(have_crypto) {
// //
function prog(nfile, nchunk, color, percent) { function prog(nfile, nchunk, color, percent) {
var n1 = o('f{0}p{1}'.format(nfile, nchunk)); var n1 = ebi('f{0}p{1}'.format(nfile, nchunk));
var n2 = n1.getElementsByTagName('div')[0]; var n2 = n1.getElementsByTagName('div')[0];
if (percent === undefined) { if (percent === undefined) {
n1.style.background = color; n1.style.background = color;
@@ -857,7 +791,7 @@ function up2k_init(have_crypto) {
dir.preventDefault(); dir.preventDefault();
} catch (ex) { } } catch (ex) { }
var obj = o('nthread'); var obj = ebi('nthread');
if (dir.target) { if (dir.target) {
obj.style.background = '#922'; obj.style.background = '#922';
var v = Math.floor(parseInt(obj.value)); var v = Math.floor(parseInt(obj.value));
@@ -892,19 +826,19 @@ function up2k_init(have_crypto) {
this.click(); this.click();
} }
o('nthread_add').onclick = function (ev) { ebi('nthread_add').onclick = function (ev) {
ev.preventDefault(); ev.preventDefault();
bumpthread(1); bumpthread(1);
}; };
o('nthread_sub').onclick = function (ev) { ebi('nthread_sub').onclick = function (ev) {
ev.preventDefault(); ev.preventDefault();
bumpthread(-1); bumpthread(-1);
}; };
o('nthread').addEventListener('input', bumpthread, false); ebi('nthread').addEventListener('input', bumpthread, false);
o('multitask').addEventListener('click', tgl_multitask, false); ebi('multitask').addEventListener('click', tgl_multitask, false);
var nodes = o('u2conf').getElementsByTagName('a'); var nodes = ebi('u2conf').getElementsByTagName('a');
for (var a = nodes.length - 1; a >= 0; a--) for (var a = nodes.length - 1; a >= 0; a--)
nodes[a].addEventListener('touchend', nop, false); nodes[a].addEventListener('touchend', nop, false);

109
copyparty/web/util.js Normal file
View File

@@ -0,0 +1,109 @@
"use strict";
// error handler for mobile devices
function hcroak(msg) {
document.body.innerHTML = msg;
window.onerror = undefined;
throw 'fatal_err';
}
function croak(msg) {
document.body.textContent = msg;
window.onerror = undefined;
throw msg;
}
function esc(txt) {
return txt.replace(/[&"<>]/g, function (c) {
return {
'&': '&amp;',
'"': '&quot;',
'<': '&lt;',
'>': '&gt;'
}[c];
});
}
function vis_exh(msg, url, lineNo, columnNo, error) {
window.onerror = undefined;
var html = ['<h1>you hit a bug!</h1><p>please screenshot this error and send me a copy arigathanks gozaimuch (ed/irc.rizon.net or ed#2644)</p><p>',
esc(String(msg)), '</p><p>', esc(url + ' @' + lineNo + ':' + columnNo), '</p>'];
if (error) {
var find = ['desc', 'stack', 'trace'];
for (var a = 0; a < find.length; a++)
if (String(error[find[a]]) !== 'undefined')
html.push('<h2>' + find[a] + '</h2>' +
esc(String(error[find[a]])).replace(/\n/g, '<br />\n'));
}
document.body.style.fontSize = '0.8em';
document.body.style.padding = '0 1em 1em 1em';
hcroak(html.join('\n'));
}
function ebi(id) {
return document.getElementById(id);
}
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/endsWith
if (!String.prototype.endsWith) {
String.prototype.endsWith = function (search, this_len) {
if (this_len === undefined || this_len > this.length) {
this_len = this.length;
}
return this.substring(this_len - search.length, this_len) === search;
};
}
if (!String.startsWith) {
String.prototype.startsWith = function (s, i) {
i = i > 0 ? i | 0 : 0;
return this.substring(i, i + s.length) === s;
};
}
// https://stackoverflow.com/a/950146
function import_js(url, cb) {
var head = document.head || document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = url;
script.onreadystatechange = cb;
script.onload = cb;
head.appendChild(script);
}
function sortTable(table, col) {
var tb = table.tBodies[0], // use `<tbody>` to ignore `<thead>` and `<tfoot>` rows
th = table.tHead.rows[0].cells,
tr = Array.prototype.slice.call(tb.rows, 0),
i, reverse = th[col].className == 'sort1' ? -1 : 1;
for (var a = 0, thl = th.length; a < thl; a++)
th[a].className = '';
th[col].className = 'sort' + reverse;
var stype = th[col].getAttribute('sort');
tr = tr.sort(function (a, b) {
var v1 = a.cells[col].textContent.trim();
var v2 = b.cells[col].textContent.trim();
if (stype == 'int') {
v1 = parseInt(v1.replace(/,/g, ''));
v2 = parseInt(v2.replace(/,/g, ''));
return reverse * (v1 - v2);
}
return reverse * (v1.localeCompare(v2));
});
for (i = 0; i < tr.length; ++i) tb.appendChild(tr[i]);
}
function makeSortable(table) {
var th = table.tHead, i;
th && (th = th.rows[0]) && (th = th.cells);
if (th) i = th.length;
else return; // if no `<thead>` then do nothing
while (--i >= 0) (function (i) {
th[i].onclick = function () {
sortTable(table, i);
};
}(i));
}

129
scripts/copyparty-repack.sh Executable file
View File

@@ -0,0 +1,129 @@
#!/bin/bash
repacker=1
set -e
# -- download latest copyparty (source.tgz and sfx),
# -- build minimal sfx versions,
# -- create a .tar.gz bundle
#
# convenient for deploying updates to inconvenient locations
# (and those are usually linux so bash is good inaff)
# (but that said this even has macos support)
#
# bundle will look like:
# -rwxr-xr-x 0 ed ed 183808 Nov 19 00:43 copyparty
# -rw-r--r-- 0 ed ed 491318 Nov 19 00:40 copyparty-extras/copyparty-0.5.4.tar.gz
# -rwxr-xr-x 0 ed ed 30254 Nov 17 23:58 copyparty-extras/copyparty-fuse.py
# -rwxr-xr-x 0 ed ed 481403 Nov 19 00:40 copyparty-extras/sfx-full/copyparty-sfx.sh
# -rwxr-xr-x 0 ed ed 506043 Nov 19 00:40 copyparty-extras/sfx-full/copyparty-sfx.py
# -rwxr-xr-x 0 ed ed 167699 Nov 19 00:43 copyparty-extras/sfx-lite/copyparty-sfx.sh
# -rwxr-xr-x 0 ed ed 183808 Nov 19 00:43 copyparty-extras/sfx-lite/copyparty-sfx.py
command -v gtar && tar() { gtar "$@"; }
command -v gsed && sed() { gsed "$@"; }
td="$(mktemp -d)"
od="$(pwd)"
cd "$td"
pwd
dl_text() {
command -v curl && exec curl "$@"
exec wget -O- "$@"
}
dl_files() {
command -v curl && exec curl -L --remote-name-all "$@"
exec wget "$@"
}
export -f dl_files
# if cache exists, use that instead of bothering github
cache="$od/.copyparty-repack.cache"
[ -e "$cache" ] &&
tar -xf "$cache" ||
{
# get download links from github
dl_text https://api.github.com/repos/9001/copyparty/releases/latest |
(
# prefer jq if available
jq -r '.assets[]|select(.name|test("-sfx|tar.gz")).browser_download_url' ||
# fallback to awk (sorry)
awk -F\" '/"browser_download_url".*(\.tar\.gz|-sfx\.)/ {print$4}'
) |
tee /dev/stderr |
tr -d '\r' | tr '\n' '\0' |
xargs -0 bash -c 'dl_files "$@"' _
tar -czf "$cache" *
}
# move src into copyparty-extras/,
# move sfx into copyparty-extras/sfx-full/
mkdir -p copyparty-extras/sfx-{full,lite}
mv copyparty-sfx.* copyparty-extras/sfx-full/
mv copyparty-*.tar.gz copyparty-extras/
# unpack the source code
( cd copyparty-extras/
tar -xf *.tar.gz
)
# use repacker from release if that is newer
p_other=copyparty-extras/copyparty-*/scripts/copyparty-repack.sh
other=$(awk -F= 'BEGIN{v=-1} NR<10&&/^repacker=/{v=$NF} END{print v}' <$p_other)
[ $repacker -lt $other ] &&
cat $p_other >"$od/$0" && cd "$od" && rm -rf "$td" && exec "$0" "$@"
# now drop the cache
rm -f "$cache"
# fix permissions
chmod 755 \
copyparty-extras/sfx-full/* \
copyparty-extras/copyparty-*/{scripts,bin}/*
# extract and repack the sfx with less features enabled
( cd copyparty-extras/sfx-full/
./copyparty-sfx.py -h
cd ../copyparty-*/
./scripts/make-sfx.sh re no-ogv no-cm
)
# put new sfx into copyparty-extras/sfx-lite/,
# fuse client into copyparty-extras/,
# copy lite-sfx.py to ./copyparty,
# delete extracted source code
( cd copyparty-extras/
mv copyparty-*/dist/* sfx-lite/
mv copyparty-*/bin/copyparty-fuse.py .
cp -pv sfx-lite/copyparty-sfx.py ../copyparty
rm -rf copyparty-{0..9}*.*.*{0..9}
)
# and include the repacker itself too
cp -av "$od/$0" copyparty-extras/ ||
cp -av "$0" copyparty-extras/ ||
true
# create the bundle
fn=copyparty-$(date +%Y-%m%d-%H%M%S).tgz
tar -czvf "$od/$fn" *
cd "$od"
rm -rf "$td"
echo
echo "done, here's your bundle:"
ls -al "$fn"

View File

@@ -4,10 +4,10 @@ import os
import time import time
""" """
mkdir -p /dev/shm/fusefuzz/{r,v} td=/dev/shm/; [ -e $td ] || td=$HOME; mkdir -p $td/fusefuzz/{r,v}
PYTHONPATH=.. python3 -m copyparty -v /dev/shm/fusefuzz/r::r -i 127.0.0.1 PYTHONPATH=.. python3 -m copyparty -v $td/fusefuzz/r::r -i 127.0.0.1
../bin/copyparty-fuse.py /dev/shm/fusefuzz/v http://127.0.0.1:3923/ 2 0 ../bin/copyparty-fuse.py http://127.0.0.1:3923/ $td/fusefuzz/v -cf 2 -cd 0.5
(d="$PWD"; cd /dev/shm/fusefuzz && "$d"/fusefuzz.py) (d="$PWD"; cd $td/fusefuzz && "$d"/fusefuzz.py)
""" """

View File

@@ -94,8 +94,39 @@ cd sfx
rm -f ../tar rm -f ../tar
} }
ver="$(awk '/^VERSION *= \(/ { ver=
gsub(/[^0-9,]/,""); gsub(/,/,"."); print; exit}' < ../copyparty/__version__.py)" git describe --tags >/dev/null 2>/dev/null && {
git_ver="$(git describe --tags)"; # v0.5.5-2-gb164aa0
ver="$(printf '%s\n' "$git_ver" | sed -r 's/^v//; s/-g?/./g')";
t_ver=
printf '%s\n' "$git_ver" | grep -qE '^v[0-9\.]+$' && {
# short format (exact version number)
t_ver="$(printf '%s\n' "$ver" | sed -r 's/\./, /g')";
}
printf '%s\n' "$git_ver" | grep -qE '^v[0-9\.]+-[0-9]+-g[0-9a-f]+$' && {
# long format (unreleased commit)
t_ver="$(printf '%s\n' "$ver" | sed -r 's/\./, /g; s/(.*) (.*)/\1 "\2"/')"
}
[ -z "$t_ver" ] && {
printf 'unexpected git version format: [%s]\n' "$git_ver"
exit 1
}
dt="$(git log -1 --format=%cd --date=format:'%Y,%m,%d' | sed -E 's/,0?/, /g')"
printf 'git %3s: \033[36m%s\033[0m\n' ver "$ver" dt "$dt"
sed -ri '
s/^(VERSION =)(.*)/#\1\2\n\1 ('"$t_ver"')/;
s/^(S_VERSION =)(.*)/#\1\2\n\1 "'"$ver"'"/;
s/^(BUILD_DT =)(.*)/#\1\2\n\1 ('"$dt"')/;
' copyparty/__version__.py
}
[ -z "$ver" ] &&
ver="$(awk '/^VERSION *= \(/ {
gsub(/[^0-9,]/,""); gsub(/,/,"."); print; exit}' < copyparty/__version__.py)"
ts=$(date -u +%s) ts=$(date -u +%s)
hts=$(date -u +%Y-%m%d-%H%M%S) # --date=@$ts (thx osx) hts=$(date -u +%Y-%m%d-%H%M%S) # --date=@$ts (thx osx)

View File

@@ -16,15 +16,15 @@ which md5sum 2>/dev/null >/dev/null &&
ver="$1" ver="$1"
[[ "x$ver" == x ]] && [ "x$ver" = x ] &&
{ {
echo "need argument 1: version" echo "need argument 1: version"
echo echo
exit 1 exit 1
} }
[[ -e copyparty/__main__.py ]] || cd .. [ -e copyparty/__main__.py ] || cd ..
[[ -e copyparty/__main__.py ]] || [ -e copyparty/__main__.py ] ||
{ {
echo "run me from within the project root folder" echo "run me from within the project root folder"
echo echo
@@ -35,8 +35,8 @@ mkdir -p dist
zip_path="$(pwd)/dist/copyparty-$ver.zip" zip_path="$(pwd)/dist/copyparty-$ver.zip"
tgz_path="$(pwd)/dist/copyparty-$ver.tar.gz" tgz_path="$(pwd)/dist/copyparty-$ver.tar.gz"
[[ -e "$zip_path" ]] || [ -e "$zip_path" ] ||
[[ -e "$tgz_path" ]] && [ -e "$tgz_path" ] &&
{ {
echo "found existing archives for this version" echo "found existing archives for this version"
echo " $zip_path" echo " $zip_path"

141
srv/extend.md Normal file
View File

@@ -0,0 +1,141 @@
# hi
this showcases my worst idea yet; *extending markdown with inline javascript*
due to obvious reasons it's disabled by default, and can be enabled with `-emp`
the examples are by no means correct, they're as much of a joke as this feature itself
### sub-header
nothing special about this one
## except/
this one becomes a hyperlink to ./except/ thanks to
* the `copyparty_pre` plugin at the end of this file
* which is invoked as a markdown filter every time the document is modified
* which looks for headers ending with a `/` and erwrites all headers below that
it is a passthrough to the markdown extension api, see https://marked.js.org/using_pro
in addition to the markdown extension functions, `ctor` will be called on document init
### these/
and this one becomes ./except/these/
#### ones.md
finally ./except/these/ones.md
### also-this.md
whic hshoud be ./except/also-this.md
# ok
now for another extension type, `copyparty_post` which is called to manipulate the generated dom instead
`copyparty_post` can have the following functions, all optional
* `ctor` is called on document init
* `render` is called when the dom is done but still in-memory
* `render2` is called with the live browser dom as-displayed
## post example
the values in the `ex:` columns are linkified to `example.com/$value`
| ex:foo | bar | ex:baz |
| ------------ | -------- | ------ |
| asdf | nice | fgsfds |
| more one row | hi hello | aaa |
and the table can be sorted by clicking the headers
the difference is that with `copyparty_pre` you'll probably break various copyparty features but if you use `copyparty_post` then future copyparty versions will probably break you
# heres the plugins
if there is anything below ths line in the preview then the plugin feature is disabled (good)
```copyparty_pre
ctor() {
md_plug['h'] = {
on: false,
lv: -1,
path: []
}
},
walkTokens(token) {
if (token.type == 'heading') {
var h = md_plug['h'],
is_dir = token.text.endsWith('/');
if (h.lv >= token.depth) {
h.on = false;
}
if (!h.on && is_dir) {
h.on = true;
h.lv = token.depth;
h.path = [token.text];
}
else if (h.on && h.lv < token.depth) {
h.path = h.path.slice(0, token.depth - h.lv);
h.path.push(token.text);
}
if (!h.on)
return false;
var path = h.path.join('');
var emoji = is_dir ? '📂' : '📜';
token.tokens[0].text = '<a href="' + path + '">' + emoji + ' ' + path + '</a>';
}
if (token.type == 'paragraph') {
//console.log(JSON.parse(JSON.stringify(token.tokens)));
for (var a = 0; a < token.tokens.length; a++) {
var t = token.tokens[a];
if (t.type == 'text' || t.type == 'strong' || t.type == 'em') {
var ret = '', text = t.text;
for (var b = 0; b < text.length; b++)
ret += (Math.random() > 0.5) ? text[b] : text[b].toUpperCase();
t.text = ret;
}
}
}
return true;
}
```
```copyparty_post
render(dom) {
var ths = dom.querySelectorAll('th');
for (var a = 0; a < ths.length; a++) {
var th = ths[a];
if (th.textContent.indexOf('ex:') === 0) {
th.textContent = th.textContent.slice(3);
var nrow = 0;
while ((th = th.previousSibling) != null)
nrow++;
var trs = ths[a].parentNode.parentNode.parentNode.querySelectorAll('tr');
for (var b = 1; b < trs.length; b++) {
var td = trs[b].childNodes[nrow];
td.innerHTML = '<a href="//example.com/' + td.innerHTML + '">' + td.innerHTML + '</a>';
}
}
}
},
render2(dom) {
window.makeSortable(dom.getElementsByTagName('table')[0]);
}
```

View File

@@ -139,6 +139,10 @@ a newline toplevel
a table | big text in this | aaakbfddd a table | big text in this | aaakbfddd
second row | centred | bbb second row | centred | bbb
||
--|--|--
foo
* list entry * list entry
* [x] yes * [x] yes
* [ ] no * [ ] no
@@ -227,3 +231,7 @@ unrelated neat stuff:
awk '/./ {printf "%s %d\n", $0, NR; next} 1' <test.md >ln.md awk '/./ {printf "%s %d\n", $0, NR; next} 1' <test.md >ln.md
gawk '{print gensub(/([a-zA-Z\.])/,NR" \\1","1")}' <test.md >ln.md gawk '{print gensub(/([a-zA-Z\.])/,NR" \\1","1")}' <test.md >ln.md
``` ```
a|b|c
--|--|--
foo

View File

@@ -3,8 +3,10 @@
from __future__ import print_function, unicode_literals from __future__ import print_function, unicode_literals
import os import os
import time
import json import json
import shutil import shutil
import tempfile
import unittest import unittest
import subprocess as sp # nosec import subprocess as sp # nosec
@@ -30,9 +32,6 @@ class TestVFS(unittest.TestCase):
response = self.unfoo(response) response = self.unfoo(response)
self.assertEqual(util.undot(query), response) self.assertEqual(util.undot(query), response)
def absify(self, root, names):
return ["{}/{}".format(root, x).replace("//", "/") for x in names]
def ls(self, vfs, vpath, uname): def ls(self, vfs, vpath, uname):
"""helper for resolving and listing a folder""" """helper for resolving and listing a folder"""
vn, rem = vfs.get(vpath, uname, True, False) vn, rem = vfs.get(vpath, uname, True, False)
@@ -59,16 +58,31 @@ class TestVFS(unittest.TestCase):
if os.path.exists("/Volumes"): if os.path.exists("/Volumes"):
devname, _ = self.chkcmd("hdiutil", "attach", "-nomount", "ram://8192") devname, _ = self.chkcmd("hdiutil", "attach", "-nomount", "ram://8192")
_, _ = self.chkcmd("diskutil", "eraseVolume", "HFS+", "cptd", devname) devname = devname.strip()
return "/Volumes/cptd" print("devname: [{}]".format(devname))
for _ in range(10):
try:
_, _ = self.chkcmd(
"diskutil", "eraseVolume", "HFS+", "cptd", devname
)
return "/Volumes/cptd"
except Exception as ex:
print(repr(ex))
time.sleep(0.25)
raise Exception("TODO support windows") raise Exception("ramdisk creation failed")
ret = os.path.join(tempfile.gettempdir(), "copyparty-test")
try:
os.mkdir(ret)
finally:
return ret
def log(self, src, msg): def log(self, src, msg):
pass pass
def test(self): def test(self):
td = self.get_ramdisk() + "/vfs" td = os.path.join(self.get_ramdisk(), "vfs")
try: try:
shutil.rmtree(td) shutil.rmtree(td)
except OSError: except OSError:
@@ -99,7 +113,7 @@ class TestVFS(unittest.TestCase):
vfs = AuthSrv(Namespace(c=None, a=[], v=["a/ab/::r"]), self.log).vfs vfs = AuthSrv(Namespace(c=None, a=[], v=["a/ab/::r"]), self.log).vfs
self.assertEqual(vfs.nodes, {}) self.assertEqual(vfs.nodes, {})
self.assertEqual(vfs.vpath, "") self.assertEqual(vfs.vpath, "")
self.assertEqual(vfs.realpath, td + "/a/ab") self.assertEqual(vfs.realpath, os.path.join(td, "a", "ab"))
self.assertEqual(vfs.uread, ["*"]) self.assertEqual(vfs.uread, ["*"])
self.assertEqual(vfs.uwrite, []) self.assertEqual(vfs.uwrite, [])
@@ -109,7 +123,7 @@ class TestVFS(unittest.TestCase):
).vfs ).vfs
self.assertEqual(vfs.nodes, {}) self.assertEqual(vfs.nodes, {})
self.assertEqual(vfs.vpath, "") self.assertEqual(vfs.vpath, "")
self.assertEqual(vfs.realpath, td + "/a/aa") self.assertEqual(vfs.realpath, os.path.join(td, "a", "aa"))
self.assertEqual(vfs.uread, ["*"]) self.assertEqual(vfs.uread, ["*"])
self.assertEqual(vfs.uwrite, []) self.assertEqual(vfs.uwrite, [])
@@ -138,42 +152,63 @@ class TestVFS(unittest.TestCase):
n = n.nodes["acb"] n = n.nodes["acb"]
self.assertEqual(n.nodes, {}) self.assertEqual(n.nodes, {})
self.assertEqual(n.vpath, "a/ac/acb") self.assertEqual(n.vpath, "a/ac/acb")
self.assertEqual(n.realpath, td + "/a/ac/acb") self.assertEqual(n.realpath, os.path.join(td, "a", "ac", "acb"))
self.assertEqual(n.uread, ["k"]) self.assertEqual(n.uread, ["k"])
self.assertEqual(n.uwrite, ["*", "k"]) self.assertEqual(n.uwrite, ["*", "k"])
# something funky about the windows path normalization,
# doesn't really matter but makes the test messy, TODO?
fsdir, real, virt = self.ls(vfs, "/", "*") fsdir, real, virt = self.ls(vfs, "/", "*")
self.assertEqual(fsdir, td) self.assertEqual(fsdir, td)
self.assertEqual(real, ["b", "c"]) self.assertEqual(real, ["b", "c"])
self.assertEqual(list(virt), ["a"]) self.assertEqual(list(virt), ["a"])
fsdir, real, virt = self.ls(vfs, "a", "*") fsdir, real, virt = self.ls(vfs, "a", "*")
self.assertEqual(fsdir, td + "/a") self.assertEqual(fsdir, os.path.join(td, "a"))
self.assertEqual(real, ["aa", "ab"]) self.assertEqual(real, ["aa", "ab"])
self.assertEqual(list(virt), ["ac"]) self.assertEqual(list(virt), ["ac"])
fsdir, real, virt = self.ls(vfs, "a/ab", "*") fsdir, real, virt = self.ls(vfs, "a/ab", "*")
self.assertEqual(fsdir, td + "/a/ab") self.assertEqual(fsdir, os.path.join(td, "a", "ab"))
self.assertEqual(real, ["aba", "abb", "abc"]) self.assertEqual(real, ["aba", "abb", "abc"])
self.assertEqual(list(virt), []) self.assertEqual(list(virt), [])
fsdir, real, virt = self.ls(vfs, "a/ac", "*") fsdir, real, virt = self.ls(vfs, "a/ac", "*")
self.assertEqual(fsdir, td + "/a/ac") self.assertEqual(fsdir, os.path.join(td, "a", "ac"))
self.assertEqual(real, ["aca", "acc"]) self.assertEqual(real, ["aca", "acc"])
self.assertEqual(list(virt), []) self.assertEqual(list(virt), [])
fsdir, real, virt = self.ls(vfs, "a/ac", "k") fsdir, real, virt = self.ls(vfs, "a/ac", "k")
self.assertEqual(fsdir, td + "/a/ac") self.assertEqual(fsdir, os.path.join(td, "a", "ac"))
self.assertEqual(real, ["aca", "acc"]) self.assertEqual(real, ["aca", "acc"])
self.assertEqual(list(virt), ["acb"]) self.assertEqual(list(virt), ["acb"])
self.assertRaises(util.Pebkac, vfs.get, "a/ac/acb", "*", True, False) self.assertRaises(util.Pebkac, vfs.get, "a/ac/acb", "*", True, False)
fsdir, real, virt = self.ls(vfs, "a/ac/acb", "k") fsdir, real, virt = self.ls(vfs, "a/ac/acb", "k")
self.assertEqual(fsdir, td + "/a/ac/acb") self.assertEqual(fsdir, os.path.join(td, "a", "ac", "acb"))
self.assertEqual(real, ["acba", "acbb", "acbc"]) self.assertEqual(real, ["acba", "acbb", "acbc"])
self.assertEqual(list(virt), []) self.assertEqual(list(virt), [])
# admin-only rootfs with all-read-only subfolder
vfs = AuthSrv(Namespace(c=None, a=["k:k"], v=[".::ak", "a:a:r"]), self.log,).vfs
self.assertEqual(len(vfs.nodes), 1)
self.assertEqual(vfs.vpath, "")
self.assertEqual(vfs.realpath, td)
self.assertEqual(vfs.uread, ["k"])
self.assertEqual(vfs.uwrite, ["k"])
n = vfs.nodes["a"]
self.assertEqual(len(vfs.nodes), 1)
self.assertEqual(n.vpath, "a")
self.assertEqual(n.realpath, os.path.join(td, "a"))
self.assertEqual(n.uread, ["*"])
self.assertEqual(n.uwrite, [])
self.assertEqual(vfs.can_access("/", "*"), [False, False])
self.assertEqual(vfs.can_access("/", "k"), [True, True])
self.assertEqual(vfs.can_access("/a", "*"), [True, False])
self.assertEqual(vfs.can_access("/a", "k"), [True, False])
# breadth-first construction # breadth-first construction
vfs = AuthSrv( vfs = AuthSrv(
Namespace( Namespace(
@@ -207,20 +242,20 @@ class TestVFS(unittest.TestCase):
self.assertEqual(list(v1), ["a"]) self.assertEqual(list(v1), ["a"])
fsp, r1, v1 = self.ls(vfs, "a", "*") fsp, r1, v1 = self.ls(vfs, "a", "*")
self.assertEqual(fsp, td + "/a") self.assertEqual(fsp, os.path.join(td, "a"))
self.assertEqual(r1, ["aa", "ab"]) self.assertEqual(r1, ["aa", "ab"])
self.assertEqual(list(v1), ["ac"]) self.assertEqual(list(v1), ["ac"])
fsp1, r1, v1 = self.ls(vfs, "a/ac", "*") fsp1, r1, v1 = self.ls(vfs, "a/ac", "*")
fsp2, r2, v2 = self.ls(vfs, "b", "*") fsp2, r2, v2 = self.ls(vfs, "b", "*")
self.assertEqual(fsp1, td + "/b") self.assertEqual(fsp1, os.path.join(td, "b"))
self.assertEqual(fsp2, td + "/b") self.assertEqual(fsp2, os.path.join(td, "b"))
self.assertEqual(r1, ["ba", "bb", "bc"]) self.assertEqual(r1, ["ba", "bb", "bc"])
self.assertEqual(r1, r2) self.assertEqual(r1, r2)
self.assertEqual(list(v1), list(v2)) self.assertEqual(list(v1), list(v2))
# config file parser # config file parser
cfg_path = self.get_ramdisk() + "/test.cfg" cfg_path = os.path.join(self.get_ramdisk(), "test.cfg")
with open(cfg_path, "wb") as f: with open(cfg_path, "wb") as f:
f.write( f.write(
dedent( dedent(
@@ -248,10 +283,11 @@ class TestVFS(unittest.TestCase):
self.assertEqual(len(n.nodes), 1) self.assertEqual(len(n.nodes), 1)
n = n.nodes["dst"] n = n.nodes["dst"]
self.assertEqual(n.vpath, "dst") self.assertEqual(n.vpath, "dst")
self.assertEqual(n.realpath, td + "/src") self.assertEqual(n.realpath, os.path.join(td, "src"))
self.assertEqual(n.uread, ["a", "asd"]) self.assertEqual(n.uread, ["a", "asd"])
self.assertEqual(n.uwrite, ["asd"]) self.assertEqual(n.uwrite, ["asd"])
self.assertEqual(len(n.nodes), 0) self.assertEqual(len(n.nodes), 0)
os.chdir(tempfile.gettempdir())
shutil.rmtree(td) shutil.rmtree(td)
os.unlink(cfg_path) os.unlink(cfg_path)