Compare commits

..

61 Commits

Author SHA1 Message Date
ed
c0de3c8053 v1.2.11 2022-05-13 17:24:50 +02:00
ed
a82a3b084a make search results unselectable 2022-05-13 17:18:19 +02:00
ed
67c298e66b don't embed huge docs (defer to ajax), closes #9 2022-05-13 17:08:17 +02:00
ed
c110ccb9ae v1.2.10 2022-05-13 01:44:00 +02:00
ed
0143380306 help the query planner 2022-05-13 01:41:39 +02:00
ed
af9000d3c8 v1.2.9 2022-05-12 23:10:54 +02:00
ed
097d798e5e steal colors from monokai 2022-05-12 23:06:37 +02:00
ed
1d9f9f221a louder 2022-05-12 20:55:37 +02:00
ed
214a367f48 be loud about segfaults and such 2022-05-12 20:26:48 +02:00
ed
2fb46551a2 avoid pointless recursion + show scan summary 2022-05-09 23:43:59 +02:00
ed
6bcf330ae0 symlink-checker: print base vpath in nonverbose mode 2022-05-09 20:17:03 +00:00
ed
2075a8b18c skip nonregular files when indexing filesystem 2022-05-09 19:56:17 +00:00
ed
1275ac6c42 start up2k indexing even if no interfaces could bind 2022-05-09 20:38:06 +02:00
ed
708f20b7af remove option to disable spa 2022-05-08 14:29:05 +02:00
ed
a2c0c708e8 focus password field if not logged in 2022-05-07 22:16:12 +02:00
ed
2f2c65d91e improve up2k error messages 2022-05-07 22:15:09 +02:00
ed
cd5fcc7ca7 fix file sel/play background on focus 2022-05-06 21:15:18 +02:00
ed
aa29e7be48 minimal support for browsers without css-variables 2022-05-03 00:52:26 +02:00
ed
93febe34b0 truncate huge ffmpeg errors 2022-05-03 00:32:00 +02:00
ed
f086e6d3c1 best-effort recovery when chrome desyncs the mediaSession 2022-05-02 19:08:37 +02:00
ed
22e51e1c96 compensate for play/pause fades by rewinding a bit 2022-05-02 19:07:16 +02:00
ed
63a5336f31 change modal ok/cancel focus with left/right keys 2022-05-02 19:06:51 +02:00
ed
bfc6c53cc5 ux 2022-05-02 19:06:08 +02:00
ed
236017f310 better dropzones on small screens 2022-05-02 01:08:31 +02:00
ed
0a1d9b4dfd nevermind, not reliable when rproxied 2022-05-01 22:35:34 +02:00
ed
b50d090946 add logout on inactivity + related errorhandling 2022-05-01 22:12:25 +02:00
ed
00b5db52cf notes 2022-05-01 12:02:27 +02:00
ed
24cb30e2c5 support login from ie4 / win3.11 2022-05-01 11:42:19 +02:00
ed
4549145ab5 fix filekeys in basic-html browser 2022-05-01 11:29:51 +02:00
ed
67b0217754 cleanup + readme 2022-04-30 23:37:27 +02:00
ed
ccae9efdf0 safer systemd example (unprivileged user + NAT for port 80 / 443) 2022-04-30 23:28:51 +02:00
ed
59d596b222 add service to autogenerate TLS certificates 2022-04-30 22:54:35 +02:00
ed
4878eb2c45 support symlinks as volume root 2022-04-30 20:26:26 +02:00
ed
7755392f57 redirect to webroot after login 2022-04-30 18:15:09 +02:00
ed
dc2ea20959 v1.2.8 2022-04-30 02:16:34 +02:00
ed
8eaea2bd17 ux 2022-04-30 00:37:31 +02:00
ed
58e559918f fix dynamic tree sizing 2022-04-30 00:04:06 +02:00
ed
f38a3fca5b case-insensitive cover check 2022-04-29 23:39:16 +02:00
ed
1ea145b384 wow when did that break 2022-04-29 23:37:38 +02:00
ed
0d9567575a avoid hashing busy uploads during rescan 2022-04-29 23:16:23 +02:00
ed
e82f176289 fix deadlock on rescan during upload 2022-04-29 23:14:51 +02:00
ed
d4b51c040e doc + ux 2022-04-29 23:13:37 +02:00
ed
125d0efbd8 good stuff 2022-04-29 02:06:56 +02:00
ed
3215afc504 immediately search on enter key 2022-04-28 22:53:37 +02:00
ed
c73ff3ce1b avoid sqlite deadlock on windows 2022-04-28 22:46:53 +02:00
ed
f9c159a051 add option to force up2k turbo + hide warning 2022-04-28 21:57:37 +02:00
ed
2ab1325c90 add option to load more search results 2022-04-28 21:55:01 +02:00
ed
5b0f7ff506 perfect 2022-04-28 10:36:56 +02:00
ed
9269bc84f2 skip more stuff windows doesn't like 2022-04-28 10:31:10 +02:00
ed
4e8b651e18 too much effort into this joke 2022-04-28 10:29:54 +02:00
ed
65b4f79534 add themes "vice" and "hot dog stand" 2022-04-27 22:33:01 +02:00
ed
5dd43dbc45 ignore bugs in chrome v102 2022-04-27 22:32:11 +02:00
ed
5f73074c7e fix audio playback on first visit 2022-04-27 22:31:33 +02:00
ed
f5d6ba27b2 handle invalid headers better 2022-04-27 22:30:19 +02:00
ed
73fa70b41f fix mostly-harmless xss 2022-04-27 22:29:16 +02:00
ed
2a1cda42e7 avoid deadlocks on windows 2022-04-27 22:27:49 +02:00
ed
1bd7e31466 more theme porting 2022-04-26 00:42:00 +02:00
ed
eb49e1fb4a conditional up2k column sizes depending on card 2022-04-24 23:48:23 +02:00
ed
9838c2f0ce golf 2022-04-24 23:47:15 +02:00
ed
6041df8370 start replacing class-scopes with css variables 2022-04-24 23:46:38 +02:00
ed
2933dce3ef mtime blank uploads + helptext 2022-04-24 22:58:11 +02:00
34 changed files with 1554 additions and 1116 deletions

View File

@@ -63,6 +63,7 @@ turn your phone or raspi into a portable file server with resumable uploads/down
* [file parser plugins](#file-parser-plugins) - provide custom parsers to index additional tags, also see [./bin/mtag/README.md](./bin/mtag/README.md)
* [upload events](#upload-events) - trigger a script/program on each upload
* [hiding from google](#hiding-from-google) - tell search engines you dont wanna be indexed
* [themes](#themes)
* [complete examples](#complete-examples)
* [browser support](#browser-support) - TLDR: yes
* [client examples](#client-examples) - interact with copyparty using non-browser clients
@@ -247,6 +248,8 @@ some improvement ideas
## not my bugs
* [Chrome issue 1317069](https://bugs.chromium.org/p/chromium/issues/detail?id=1317069) -- if you try to upload a folder which contains symlinks by dragging it into the browser, the symlinked files will not get uploaded
* iPhones: the volume control doesn't work because [apple doesn't want it to](https://developer.apple.com/library/archive/documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/Device-SpecificConsiderations/Device-SpecificConsiderations.html#//apple_ref/doc/uid/TP40009523-CH5-SW11)
* *future workaround:* enable the equalizer, make it all-zero, and set a negative boost to reduce the volume
* "future" because `AudioContext` is broken in the current iOS version (15.1), maybe one day...
@@ -806,6 +809,29 @@ tell search engines you dont wanna be indexed, either using the good old [robot
also, `--force-js` disables the plain HTML folder listing, making things harder to parse for search engines
## themes
you can change the default theme with `--theme 2`, and add your own themes by modifying `browser.css` or providing your own css to `--css-browser`, then telling copyparty they exist by increasing `--themes`
<table><tr><td width="33%" align="center"><a href="https://user-images.githubusercontent.com/241032/165864907-17e2ac7d-319d-4f25-8718-2f376f614b51.png"><img src="https://user-images.githubusercontent.com/241032/165867551-fceb35dd-38f0-42bb-bef3-25ba651ca69b.png"></a>
0. classic dark</td><td width="33%" align="center"><a href="https://user-images.githubusercontent.com/241032/165864904-c5b67ddd-f383-4b9e-9f5a-a3bde183d256.png"><img src="https://user-images.githubusercontent.com/241032/165867556-077b6068-2488-4fae-bf88-1fce40e719bc.png"></a>
2. flat dark</td><td width="33%" align="center"><a href="https://user-images.githubusercontent.com/241032/165864901-db13a429-a5da-496d-8bc6-ce838547f69d.png"><img src="https://user-images.githubusercontent.com/241032/165867560-aa834aef-58dc-4abe-baef-7e562b647945.png"></a>
4. vice</td></tr><tr><td align="center"><a href="https://user-images.githubusercontent.com/241032/165864905-692682eb-6fb4-4d40-b6fe-27d2c7d3e2a7.png"><img src="https://user-images.githubusercontent.com/241032/165867555-080b73b6-6d85-41bb-a7c6-ad277c608365.png"></a>
1. classic light</td><td align="center"><a href="https://user-images.githubusercontent.com/241032/165864903-7fba1cb9-036b-4f11-90d5-28b7c0724353.png"><img src="https://user-images.githubusercontent.com/241032/165867557-b5cc0010-d880-48b1-8156-9c84f7bbc521.png"></a>
3. flat light
</td><td align="center"><a href="https://user-images.githubusercontent.com/241032/165864898-10ce7052-a117-4fcf-845b-b56c91687908.png"><img src="https://user-images.githubusercontent.com/241032/165867562-f3003d45-dd2a-4564-8aae-fed44c1ae064.png"></a>
5. <a href="https://blog.codinghorror.com/a-tribute-to-the-windows-31-hot-dog-stand-color-scheme/">hotdog stand</a></td></tr></table>
the classname of the HTML tag is set according to the selected theme, which is used to set colors as css variables ++
* each theme *generally* has a dark theme (even numbers) and a light theme (odd numbers), showing in pairs
* the first theme (theme 0 and 1) is `html.a`, second theme (2 and 3) is `html.b`
* if a light theme is selected, `html.y` is set, otherwise `html.z` is
* so if the dark edition of the 2nd theme is selected, you use any of `html.b`, `html.z`, `html.bz` to specify rules
see the top of [./copyparty/web/browser.css](./copyparty/web/browser.css) where the color variables are set, and there's layout-specific stuff near the bottom
## complete examples
* read-only music server with bpm and key scanning
@@ -855,7 +881,7 @@ quick summary of more eccentric web-browsers trying to view a directory index:
| **w3m** (0.5.3/macports) | can browse, login, upload at 100kB/s, mkdir/msg |
| **netsurf** (3.10/arch) | is basically ie6 with much better css (javascript has almost no effect) |
| **opera** (11.60/winxp) | OK: thumbnails, image-viewer, zip-selection, rename/cut/paste. NG: up2k, navpane, markdown, audio |
| **ie4** and **netscape** 4.0 | can browse, upload with `?b=u` |
| **ie4** and **netscape** 4.0 | can browse, upload with `?b=u`, auth with `&pw=wark` |
| **SerenityOS** (7e98457) | hits a page fault, works with `?b=u`, file upload not-impl |
@@ -1149,12 +1175,16 @@ for the `re`pack to work, first run one of the sfx'es once to unpack it
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
apt update && apt -y full-upgrade && termux-setup-storage && apt -y install python && python -m ensurepip && python -m pip install -U copyparty
apt update && apt -y full-upgrade && apt update && termux-setup-storage && apt -y install python && python -m ensurepip && python -m pip install --user -U copyparty
echo $?
```
after the initial setup, you can launch copyparty at any time by running `copyparty` anywhere in Termux
if you want thumbnails, `apt -y install ffmpeg`
* or if you want to use vips instead, `apt -y install libvips && python -m pip install --user -U wheel && python -m pip install --user -U pyvips && (cd /data/data/com.termux/files/usr/lib/; ln -s libgobject-2.0.so{,.0}; ln -s libvips.so{,.42})`
# reporting bugs
@@ -1178,7 +1208,7 @@ python3 -m venv .venv
pip install jinja2 # mandatory
pip install mutagen # audio metadata
pip install Pillow pyheif-pillow-opener pillow-avif-plugin # thumbnails
pip install black bandit pylint flake8 # vscode tooling
pip install black==21.12b0 bandit pylint flake8 # vscode tooling
```

View File

@@ -29,6 +29,7 @@ however if your copyparty is behind a reverse-proxy, you may want to use [`share
### [`cfssl.sh`](cfssl.sh)
* creates CA and server certificates using cfssl
* give a 3rd argument to install it to your copyparty config
* systemd service at [`systemd/cfssl.service`](systemd/cfssl.service)
# OS integration
init-scripts to start copyparty as a service

View File

@@ -7,7 +7,7 @@ srv_fqdn="$2"
[ -z "$srv_fqdn" ] && {
echo "need arg 1: ca name"
echo "need arg 2: server fqdn"
echo "need arg 2: server fqdn and/or IPs, comma-separated"
echo "optional arg 3: if set, write cert into copyparty cfg"
exit 1
}

View File

@@ -0,0 +1,23 @@
# systemd service which generates a new TLS certificate on each boot,
# that way the one-year expiry time won't cause any issues --
# just have everyone trust the ca.pem once every 10 years
#
# assumptions/placeholder values:
# * this script and copyparty runs as user "cpp"
# * copyparty repo is at ~cpp/dev/copyparty
# * CA is named partylan
# * server IPs = 10.1.2.3 and 192.168.123.1
# * server hostname = party.lan
[Unit]
Description=copyparty certificate generator
Before=copyparty.service
[Service]
User=cpp
Type=oneshot
SyslogIdentifier=cpp-cert
ExecStart=/bin/bash -c 'cd ~/dev/copyparty/contrib && ./cfssl.sh partylan 10.1.2.3,192.168.123.1,party.lan y'
[Install]
WantedBy=multi-user.target

View File

@@ -2,16 +2,22 @@
# and share '/mnt' with anonymous read+write
#
# installation:
# cp -pv copyparty.service /etc/systemd/system && systemctl enable --now copyparty
# cp -pv copyparty.service /etc/systemd/system
# restorecon -vr /etc/systemd/system/copyparty.service
# firewall-cmd --permanent --add-port={80,443,3923}/tcp
# firewall-cmd --reload
# systemctl daemon-reload && systemctl enable --now copyparty
#
# you may want to:
# change "User=cpp" and "/home/cpp/" to another user
# remove the nft lines to only listen on port 3923
# and in the ExecStart= line:
# change '/usr/bin/python3' to another interpreter
# change '/mnt::rw' to another location or permission-set
# remove '-p 80,443,3923' to only listen on port 3923
# add '-q' to disable logging on busy servers
# add '-i 127.0.0.1' to only allow local connections
# add '-e2dsa' to enable filesystem scanning + indexing
# add '-e2ts' to enable metadata indexing
#
# with `Type=notify`, copyparty will signal systemd when it is ready to
# accept connections; correctly delaying units depending on copyparty.
@@ -19,8 +25,8 @@
# python disabling line-buffering, so messages are out-of-order:
# https://user-images.githubusercontent.com/241032/126040249-cb535cc7-c599-4931-a796-a5d9af691bad.png
#
# if you remove -q to enable logging, you may also want to remove the
# following line to enable buffering (slightly better performance):
# unless you add -q to disable logging, you may want to remove the
# following line to allow buffering (slightly better performance):
# Environment=PYTHONUNBUFFERED=x
#
# keep ExecStartPre before ExecStart, at least on rhel8
@@ -33,8 +39,23 @@ Type=notify
SyslogIdentifier=copyparty
Environment=PYTHONUNBUFFERED=x
ExecReload=/bin/kill -s USR1 $MAINPID
ExecStartPre=/bin/bash -c 'mkdir -p /run/tmpfiles.d/ && echo "x /tmp/pe-copyparty*" > /run/tmpfiles.d/copyparty.conf'
ExecStart=/usr/bin/python3 /usr/local/bin/copyparty-sfx.py -q -p 80,443,3923 -v /mnt::rw
# user to run as + where the TLS certificate is (if any)
User=cpp
Environment=XDG_CONFIG_HOME=/home/cpp/.config
# setup forwarding from ports 80 and 443 to port 3923
ExecStartPre=+/bin/bash -c 'nft -n -a list table nat | awk "/ to :3923 /{print\$NF}" | xargs -rL1 nft delete rule nat prerouting handle; true'
ExecStartPre=+nft add table ip nat
ExecStartPre=+nft -- add chain ip nat prerouting { type nat hook prerouting priority -100 \; }
ExecStartPre=+nft add rule ip nat prerouting tcp dport 80 redirect to :3923
ExecStartPre=+nft add rule ip nat prerouting tcp dport 443 redirect to :3923
# stop systemd-tmpfiles-clean.timer from deleting copyparty while it's running
ExecStartPre=+/bin/bash -c 'mkdir -p /run/tmpfiles.d/ && echo "x /tmp/pe-copyparty*" > /run/tmpfiles.d/copyparty.conf'
# copyparty settings
ExecStart=/usr/bin/python3 /usr/local/bin/copyparty-sfx.py -e2d -v /mnt::rw
[Install]
WantedBy=multi-user.target

View File

@@ -291,9 +291,9 @@ def run_argparse(argv, formatter):
dedent(
"""
-a takes username:password,
-v takes src:dst:perm1:perm2:permN:volflag1:volflag2:volflagN:...
where "perm" is "permissions,username1,username2,..."
and "volflag" is config flags to set on this volume
-v takes src:dst:\033[33mperm\033[0m1:\033[33mperm\033[0m2:\033[33mperm\033[0mN:\033[32mvolflag\033[0m1:\033[32mvolflag\033[0m2:\033[32mvolflag\033[0mN:...
* "\033[33mperm\033[0m" is "permissions,username1,username2,..."
* "\033[32mvolflag\033[0m" is config flags to set on this volume
list of permissions:
"r" (read): list folder contents, download files
@@ -365,6 +365,17 @@ def run_argparse(argv, formatter):
generate ".bpm" tags from uploads (f = overwrite tags)
\033[36mmtp=ahash,vhash=media-hash.py\033[35m collects two tags at once
\033[0mthumbnails:
\033[36mdthumb\033[35m disables all thumbnails
\033[36mdvthumb\033[35m disables video thumbnails
\033[36mdathumb\033[35m disables audio thumbnails (spectrograms)
\033[36mdithumb\033[35m disables image thumbnails
\033[0mclient and ux:
\033[36mhtml_head=TXT\033[35m includes TXT in the <head>
\033[36mrobots\033[35m allows indexing by search engines (default)
\033[36mnorobots\033[35m kindly asks search engines to leave
\033[0mothers:
\033[36mfk=8\033[35m generates per-file accesskeys,
which will then be required at the "g" permission
@@ -373,7 +384,7 @@ def run_argparse(argv, formatter):
],
[
"urlform",
"",
"how to handle url-form POSTs",
dedent(
"""
values for --urlform:
@@ -412,40 +423,41 @@ def run_argparse(argv, formatter):
ap2.add_argument("-c", metavar="PATH", type=u, action="append", help="add config file")
ap2.add_argument("-nc", metavar="NUM", type=int, default=64, help="max num clients")
ap2.add_argument("-j", metavar="CORES", type=int, default=1, help="max num cpu cores, 0=all")
ap2.add_argument("-a", metavar="ACCT", type=u, action="append", help="add account, USER:PASS; example [ed:wark")
ap2.add_argument("-v", metavar="VOL", type=u, action="append", help="add volume, SRC:DST:FLAG; example [.::r], [/mnt/nas/music:/music:r:aed")
ap2.add_argument("-ed", action="store_true", help="enable ?dots")
ap2.add_argument("-emp", action="store_true", help="enable markdown plugins")
ap2.add_argument("-a", metavar="ACCT", type=u, action="append", help="add account, USER:PASS; example [ed:wark]")
ap2.add_argument("-v", metavar="VOL", type=u, action="append", help="add volume, SRC:DST:FLAG; examples [.::r], [/mnt/nas/music:/music:r:aed]")
ap2.add_argument("-ed", action="store_true", help="enable the ?dots url parameter / client option which allows clients to see dotfiles / hidden files")
ap2.add_argument("-emp", action="store_true", help="enable markdown plugins -- neat but dangerous, big XSS risk")
ap2.add_argument("-mcr", metavar="SEC", type=int, default=60, help="md-editor mod-chk rate")
ap2.add_argument("--urlform", metavar="MODE", type=u, default="print,get", help="how to handle url-forms; examples: [stash], [save,get]")
ap2.add_argument("--urlform", metavar="MODE", type=u, default="print,get", help="how to handle url-form POSTs; see --help-urlform")
ap2.add_argument("--wintitle", metavar="TXT", type=u, default="cpp @ $pub", help="window title, for example '$ip-10.1.2.' or '$ip-'")
ap2 = ap.add_argument_group('upload options')
ap2.add_argument("--dotpart", action="store_true", help="dotfile incomplete uploads")
ap2.add_argument("--sparse", metavar="MiB", type=int, default=4, help="up2k min.size threshold (mswin-only)")
ap2.add_argument("--dotpart", action="store_true", help="dotfile incomplete uploads, hiding them from clients unless -ed")
ap2.add_argument("--sparse", metavar="MiB", type=int, default=4, help="windows-only: minimum size of incoming uploads through up2k before they are made into sparse files")
ap2.add_argument("--unpost", metavar="SEC", type=int, default=3600*12, help="grace period where uploads can be deleted by the uploader, even without delete permissions; 0=disabled")
ap2.add_argument("--no-fpool", action="store_true", help="disable file-handle pooling -- instead, repeatedly close and reopen files during upload")
ap2.add_argument("--use-fpool", action="store_true", help="force file-handle pooling, even if copyparty thinks you're better off without")
ap2.add_argument("--hardlink", action="store_true", help="prefer hardlinks instead of symlinks when possible (same filesystem)")
ap2.add_argument("--use-fpool", action="store_true", help="force file-handle pooling, even if copyparty thinks you're better off without -- probably useful on nfs and cow filesystems (zfs, btrfs)")
ap2.add_argument("--hardlink", action="store_true", help="prefer hardlinks instead of symlinks when possible (within same filesystem)")
ap2.add_argument("--never-symlink", action="store_true", help="do not fallback to symlinks when a hardlink cannot be made")
ap2.add_argument("--no-dedup", action="store_true", help="disable symlink/hardlink creation; copy file contents instead")
ap2.add_argument("--reg-cap", metavar="N", type=int, default=9000, help="max number of uploads to keep in memory when running without -e2d")
ap2.add_argument("--turbo", metavar="LVL", type=int, default=0, help="configure turbo-mode in up2k client; 0 = off and warn if enabled, 1 = off, 2 = on, 3 = on and disable datecheck")
ap2 = ap.add_argument_group('network options')
ap2.add_argument("-i", metavar="IP", type=u, default="0.0.0.0", help="ip to bind (comma-sep.)")
ap2.add_argument("-p", metavar="PORT", type=u, default="3923", help="ports to bind (comma/range)")
ap2.add_argument("--rproxy", metavar="DEPTH", type=int, default=1, help="which ip to keep; 0 = tcp, 1 = origin (first x-fwd), 2 = cloudflare, 3 = nginx, -1 = closest proxy")
ap2.add_argument("--s-wr-sz", metavar="B", type=int, default=256*1024, help="socket write size in bytes")
ap2.add_argument("--s-wr-slp", metavar="SEC", type=float, default=0, help="socket write delay in seconds")
ap2.add_argument("--rsp-slp", metavar="SEC", type=float, default=0, help="response delay in seconds")
ap2.add_argument("--s-wr-slp", metavar="SEC", type=float, default=0, help="debug: socket write delay in seconds")
ap2.add_argument("--rsp-slp", metavar="SEC", type=float, default=0, help="debug: response delay in seconds")
ap2 = ap.add_argument_group('SSL/TLS options')
ap2.add_argument("--http-only", action="store_true", help="disable ssl/tls")
ap2.add_argument("--https-only", action="store_true", help="disable plaintext")
ap2.add_argument("--http-only", action="store_true", help="disable ssl/tls -- force plaintext")
ap2.add_argument("--https-only", action="store_true", help="disable plaintext -- force tls")
ap2.add_argument("--ssl-ver", metavar="LIST", type=u, help="set allowed ssl/tls versions; [help] shows available versions; default is what your python version considers safe")
ap2.add_argument("--ciphers", metavar="LIST", type=u, help="set allowed ssl/tls ciphers; [help] shows available ciphers")
ap2.add_argument("--ssl-dbg", action="store_true", help="dump some tls info")
ap2.add_argument("--ssl-log", metavar="PATH", type=u, help="log master secrets")
ap2.add_argument("--ssl-log", metavar="PATH", type=u, help="log master secrets for later decryption in wireshark")
ap2 = ap.add_argument_group('FTP options')
ap2.add_argument("--ftp", metavar="PORT", type=int, help="enable FTP server on PORT, for example 3921")
@@ -455,26 +467,27 @@ def run_argparse(argv, formatter):
ap2.add_argument("--ftp-pr", metavar="P-P", type=u, help="the range of TCP ports to use for passive connections, for example 12000-13000")
ap2 = ap.add_argument_group('opt-outs')
ap2.add_argument("-nw", action="store_true", help="disable writes (benchmark)")
ap2.add_argument("--keep-qem", action="store_true", help="do not disable quick-edit-mode on windows")
ap2.add_argument("-nw", action="store_true", help="never write anything to disk (debug/benchmark)")
ap2.add_argument("--keep-qem", action="store_true", help="do not disable quick-edit-mode on windows (it is disabled to avoid accidental text selection which will deadlock copyparty)")
ap2.add_argument("--no-del", action="store_true", help="disable delete operations")
ap2.add_argument("--no-mv", action="store_true", help="disable move/rename operations")
ap2.add_argument("-nih", action="store_true", help="no info hostname")
ap2.add_argument("-nid", action="store_true", help="no info disk-usage")
ap2.add_argument("-nih", action="store_true", help="no info hostname -- don't show in UI")
ap2.add_argument("-nid", action="store_true", help="no info disk-usage -- don't show in UI")
ap2.add_argument("--no-zip", action="store_true", help="disable download as zip/tar")
ap2.add_argument("--no-lifetime", action="store_true", help="disable automatic deletion of uploads after a certain time (lifetime volflag)")
ap2 = ap.add_argument_group('safety options')
ap2.add_argument("--ls", metavar="U[,V[,F]]", type=u, help="scan all volumes; arguments USER,VOL,FLAGS; example [**,*,ln,p,r]")
ap2.add_argument("--salt", type=u, default="hunter2", help="up2k file-hash salt")
ap2.add_argument("--fk-salt", metavar="SALT", type=u, default=fk_salt, help="per-file accesskey salt")
ap2.add_argument("--ls", metavar="U[,V[,F]]", type=u, help="do a sanity/safety check of all volumes on startup; arguments USER,VOL,FLAGS; example [**,*,ln,p,r]")
ap2.add_argument("--salt", type=u, default="hunter2", help="up2k file-hash salt; used to generate unpredictable internal identifiers for uploads -- doesn't really matter")
ap2.add_argument("--fk-salt", metavar="SALT", type=u, default=fk_salt, help="per-file accesskey salt; used to generate unpredictable URLs for hidden files -- this one DOES matter")
ap2.add_argument("--no-dot-mv", action="store_true", help="disallow moving dotfiles; makes it impossible to move folders containing dotfiles")
ap2.add_argument("--no-dot-ren", action="store_true", help="disallow renaming dotfiles; makes it impossible to make something a dotfile")
ap2.add_argument("--no-logues", action="store_true", help="disable rendering .prologue/.epilogue.html into directory listings")
ap2.add_argument("--no-readme", action="store_true", help="disable rendering readme.md into directory listings")
ap2.add_argument("--vague-403", action="store_true", help="send 404 instead of 403 (security through ambiguity, very enterprise)")
ap2.add_argument("--force-js", action="store_true", help="don't send HTML folder listings, force clients to use the embedded json instead")
ap2.add_argument("--force-js", action="store_true", help="don't send folder listings as HTML, force clients to use the embedded json instead -- slight protection against misbehaving search engines which ignore --no-robots")
ap2.add_argument("--no-robots", action="store_true", help="adds http and html headers asking search engines to not index anything")
ap2.add_argument("--logout", metavar="H", type=float, default="8086", help="logout clients after H hours of inactivity (0.0028=10sec, 0.1=6min, 24=day, 168=week, 720=month, 8760=year)")
ap2 = ap.add_argument_group('yolo options')
ap2.add_argument("--ign-ebind", action="store_true", help="continue running even if it's impossible to listen on some of the requested endpoints")
@@ -484,8 +497,8 @@ def run_argparse(argv, formatter):
ap2.add_argument("-q", action="store_true", help="quiet")
ap2.add_argument("-lo", metavar="PATH", type=u, help="logfile, example: cpp-%%Y-%%m%%d-%%H%%M%%S.txt.xz")
ap2.add_argument("--no-voldump", action="store_true", help="do not list volumes and permissions on startup")
ap2.add_argument("--log-conn", action="store_true", help="print tcp-server msgs")
ap2.add_argument("--log-htp", action="store_true", help="print http-server threadpool scaling")
ap2.add_argument("--log-conn", action="store_true", help="debug: print tcp-server msgs")
ap2.add_argument("--log-htp", action="store_true", help="debug: print http-server threadpool scaling")
ap2.add_argument("--ihead", metavar="HEADER", type=u, action='append', help="dump incoming header")
ap2.add_argument("--lf-url", metavar="RE", type=u, default=r"^/\.cpr/|\?th=[wj]$", help="dont log URLs matching")
@@ -502,15 +515,15 @@ def run_argparse(argv, formatter):
ap2.add_argument("--th-mt", metavar="CORES", type=int, default=cores, help="num cpu cores to use for generating thumbnails")
ap2.add_argument("--th-convt", metavar="SEC", type=int, default=60, help="conversion timeout in seconds")
ap2.add_argument("--th-no-crop", action="store_true", help="dynamic height; show full image")
ap2.add_argument("--th-dec", metavar="LIBS", default="vips,pil,ff", help="decoders, in order of preference")
ap2.add_argument("--th-dec", metavar="LIBS", default="vips,pil,ff", help="image decoders, in order of preference")
ap2.add_argument("--th-no-jpg", action="store_true", help="disable jpg output")
ap2.add_argument("--th-no-webp", action="store_true", help="disable webp output")
ap2.add_argument("--th-ff-jpg", action="store_true", help="force jpg for video thumbs")
ap2.add_argument("--th-ff-jpg", action="store_true", help="force jpg output for video thumbs")
ap2.add_argument("--th-ff-swr", action="store_true", help="use swresample instead of soxr for audio thumbs")
ap2.add_argument("--th-poke", metavar="SEC", type=int, default=300, help="activity labeling cooldown")
ap2.add_argument("--th-poke", metavar="SEC", type=int, default=300, help="activity labeling cooldown -- avoids doing keepalive pokes (updating the mtime) on thumbnail folders more often than SEC seconds")
ap2.add_argument("--th-clean", metavar="SEC", type=int, default=43200, help="cleanup interval; 0=disabled")
ap2.add_argument("--th-maxage", metavar="SEC", type=int, default=604800, help="max folder age")
ap2.add_argument("--th-covers", metavar="N,N", type=u, default="folder.png,folder.jpg,cover.png,cover.jpg", help="folder thumbnails to stat for")
ap2.add_argument("--th-maxage", metavar="SEC", type=int, default=604800, help="max folder age -- folders which haven't been poked for longer than --th-poke seconds will get deleted every --th-clean seconds")
ap2.add_argument("--th-covers", metavar="N,N", type=u, default="folder.png,folder.jpg,cover.png,cover.jpg", help="folder thumbnails to stat/look for")
# https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html
# https://github.com/libvips/libvips
# ffmpeg -hide_banner -demuxers | awk '/^ D /{print$2}' | while IFS= read -r x; do ffmpeg -hide_banner -h demuxer=$x; done | grep -E '^Demuxer |extensions:'
@@ -522,46 +535,47 @@ def run_argparse(argv, formatter):
ap2 = ap.add_argument_group('transcoding options')
ap2.add_argument("--no-acode", action="store_true", help="disable audio transcoding")
ap2.add_argument("--ac-maxage", metavar="SEC", type=int, default=86400, help="delete transcode output after SEC seconds")
ap2.add_argument("--ac-maxage", metavar="SEC", type=int, default=86400, help="delete cached transcode output after SEC seconds")
ap2 = ap.add_argument_group('general db options')
ap2.add_argument("-e2d", action="store_true", help="enable up2k database")
ap2.add_argument("-e2ds", action="store_true", help="enable up2k db-scanner, sets -e2d")
ap2.add_argument("-e2dsa", action="store_true", help="scan all folders (for search), sets -e2ds")
ap2.add_argument("-e2d", action="store_true", help="enable up2k database, making files searchable + enables upload deduplocation")
ap2.add_argument("-e2ds", action="store_true", help="scan writable folders for new files on startup; sets -e2d")
ap2.add_argument("-e2dsa", action="store_true", help="scans all folders on startup; sets -e2ds")
ap2.add_argument("--hist", metavar="PATH", type=u, help="where to store volume data (db, thumbs)")
ap2.add_argument("--no-hash", metavar="PTN", type=u, help="regex: disable hashing of matching paths during e2ds folder scans")
ap2.add_argument("--no-idx", metavar="PTN", type=u, help="regex: disable indexing of matching paths during e2ds folder scans")
ap2.add_argument("--re-maxage", metavar="SEC", type=int, default=0, help="disk rescan volume interval, 0=off, can be set per-volume with the 'scan' volflag")
ap2.add_argument("--srch-time", metavar="SEC", type=int, default=30, help="search deadline")
ap2.add_argument("--srch-hits", metavar="N", type=int, default=1000, help="max search results")
ap2.add_argument("--srch-time", metavar="SEC", type=int, default=30, help="search deadline -- terminate searches running for more than SEC seconds")
ap2.add_argument("--srch-hits", metavar="N", type=int, default=7999, help="max search results to allow clients to fetch; 125 results will be shown initially")
ap2 = ap.add_argument_group('metadata db options')
ap2.add_argument("-e2t", action="store_true", help="enable metadata indexing")
ap2.add_argument("-e2ts", action="store_true", help="enable metadata scanner, sets -e2t")
ap2.add_argument("-e2tsr", action="store_true", help="rescan all metadata, sets -e2ts")
ap2.add_argument("--no-mutagen", action="store_true", help="use FFprobe for tags instead")
ap2.add_argument("--no-mtag-ff", action="store_true", help="never use FFprobe as tag reader")
ap2.add_argument("-e2t", action="store_true", help="enable metadata indexing; makes it possible to search for artist/title/codec/resolution/...")
ap2.add_argument("-e2ts", action="store_true", help="scan existing files on startup; sets -e2t")
ap2.add_argument("-e2tsr", action="store_true", help="delete all metadata from DB and do a full rescan; sets -e2ts")
ap2.add_argument("--no-mutagen", action="store_true", help="use FFprobe for tags instead; will catch more tags")
ap2.add_argument("--no-mtag-ff", action="store_true", help="never use FFprobe as tag reader; is probably safer")
ap2.add_argument("--mtag-mt", metavar="CORES", type=int, default=cores, help="num cpu cores to use for tag scanning")
ap2.add_argument("-mtm", metavar="M=t,t,t", type=u, action="append", help="add/replace metadata mapping")
ap2.add_argument("-mte", metavar="M,M,M", type=u, help="tags to index/display (comma-sep.)",
default="circle,album,.tn,artist,title,.bpm,key,.dur,.q,.vq,.aq,vc,ac,res,.fps,ahash,vhash")
ap2.add_argument("-mth", metavar="M,M,M", type=u, help="tags to hide by default (comma-sep.)",
default=".vq,.aq,vc,ac,res,.fps")
ap2.add_argument("-mtp", metavar="M=[f,]bin", type=u, action="append", help="read tag M using bin")
ap2.add_argument("-mtp", metavar="M=[f,]BIN", type=u, action="append", help="read tag M using program BIN to parse the file")
ap2 = ap.add_argument_group('ui options')
ap2.add_argument("--theme", metavar="NUM", type=int, default=0, help="default theme to use")
ap2.add_argument("--themes", metavar="NUM", type=int, default=4, help="number of themes installed")
ap2.add_argument("--themes", metavar="NUM", type=int, default=6, help="number of themes installed")
ap2.add_argument("--js-browser", metavar="L", type=u, help="URL to additional JS to include")
ap2.add_argument("--css-browser", metavar="L", type=u, help="URL to additional CSS to include")
ap2.add_argument("--html-head", metavar="TXT", type=u, default="", help="text to append to the <head> of all HTML pages")
ap2.add_argument("--textfiles", metavar="CSV", type=u, default="txt,nfo,diz,cue,readme", help="file extensions to present as plaintext")
ap2.add_argument("--txt-max", metavar="KiB", type=int, default=64, help="max size of embedded textfiles on ?doc= (anything bigger will be lazy-loaded by JS)")
ap2.add_argument("--doctitle", metavar="TXT", type=u, default="copyparty", help="title / service-name to show in html documents")
ap2 = ap.add_argument_group('debug options')
ap2.add_argument("--no-sendfile", action="store_true", help="disable sendfile")
ap2.add_argument("--no-scandir", action="store_true", help="disable scandir")
ap2.add_argument("--no-fastboot", action="store_true", help="wait for up2k indexing")
ap2.add_argument("--no-sendfile", action="store_true", help="disable sendfile; instead using a traditional file read loop")
ap2.add_argument("--no-scandir", action="store_true", help="disable scandir; instead using listdir + stat on each file")
ap2.add_argument("--no-fastboot", action="store_true", help="wait for up2k indexing before starting the httpd")
ap2.add_argument("--no-htp", action="store_true", help="disable httpserver threadpool, create threads as-needed instead")
ap2.add_argument("--stackmon", metavar="P,S", type=u, help="write stacktrace to Path every S second")
ap2.add_argument("--log-thrs", metavar="SEC", type=float, help="list active threads every SEC")

View File

@@ -1,8 +1,8 @@
# coding: utf-8
VERSION = (1, 2, 7)
VERSION = (1, 2, 11)
CODENAME = "ftp btw"
BUILD_DT = (2022, 4, 16)
BUILD_DT = (2022, 5, 13)
S_VERSION = ".".join(map(str, VERSION))
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)

View File

@@ -11,12 +11,13 @@ import hashlib
import threading
from datetime import datetime
from .__init__ import WINDOWS
from .__init__ import ANYWIN, WINDOWS
from .util import (
IMPLICATIONS,
META_NOBOTS,
uncyg,
undot,
relchk,
unhumanize,
absreal,
Pebkac,
@@ -335,6 +336,12 @@ class VFS(object):
):
# type: (str, str, bool, bool, bool, bool, bool) -> tuple[VFS, str]
"""returns [vfsnode,fs_remainder] if user has the requested permissions"""
if ANYWIN:
mod = relchk(vpath)
if mod:
self.log("vfs", "invalid relpath [{}]".format(vpath))
raise Pebkac(404)
vn, rem = self._find(vpath)
c = vn.axs
@@ -404,7 +411,7 @@ class VFS(object):
return [abspath, real, virt_vis]
def walk(self, rel, rem, seen, uname, permsets, dots, scandir, lstat):
def walk(self, rel, rem, seen, uname, permsets, dots, scandir, lstat, subvols=True):
"""
recursively yields from ./rem;
rel is a unix-style user-defined vpath (not vfs-related)
@@ -437,9 +444,14 @@ class VFS(object):
wrel = (rel + "/" + rdir).lstrip("/")
wrem = (rem + "/" + rdir).lstrip("/")
for x in self.walk(wrel, wrem, seen, uname, permsets, dots, scandir, lstat):
for x in self.walk(
wrel, wrem, seen, uname, permsets, dots, scandir, lstat, subvols
):
yield x
if not subvols:
return
for n, vfs in sorted(vfs_virt.items()):
if not dots and n.startswith("."):
continue
@@ -1100,7 +1112,7 @@ class AuthSrv(object):
flag_p = "p" in flags
flag_r = "r" in flags
n_bads = 0
bads = []
for v in vols:
v = v[1:]
vtop = "/{}/".format(v) if v else "/"
@@ -1112,10 +1124,19 @@ class AuthSrv(object):
continue
atop = vn.realpath
safeabs = atop + os.sep
g = vn.walk(
vn.vpath, "", [], u, [[True]], True, not self.args.no_scandir, False
vn.vpath,
"",
[],
u,
[[True]],
True,
not self.args.no_scandir,
False,
False,
)
for _, _, vpath, apath, files, _, _ in g:
for _, _, vpath, apath, files, dirs, _ in g:
fnames = [n[0] for n in files]
vpaths = [vpath + "/" + n for n in fnames] if vpath else fnames
vpaths = [vtop + x for x in vpaths]
@@ -1123,21 +1144,28 @@ class AuthSrv(object):
files = [[vpath + "/", apath + os.sep]] + list(zip(vpaths, apaths))
if flag_ln:
files = [x for x in files if not x[1].startswith(atop + os.sep)]
n_bads += len(files)
files = [x for x in files if not x[1].startswith(safeabs)]
if files:
dirs[:] = [] # stop recursion
bads.append(files[0][0])
if flag_v:
msg = [
if not files:
continue
elif flag_v:
msg = [""] + [
'# user "{}", vpath "{}"\n{}'.format(u, vp, ap)
for vp, ap in files
]
else:
msg = [x[1] for x in files]
msg = ["user {}, vol {}: {} =>".format(u, vtop, files[0][0])]
msg += [x[1] for x in files]
if msg:
self.log("\n" + "\n".join(msg))
self.log("\n".join(msg))
if n_bads and flag_p:
if bads:
self.log("\n ".join(["found symlinks leaving volume:"] + bads))
if bads and flag_p:
raise Exception("found symlink leaving volume, and strict is set")
if not flag_r:

View File

@@ -121,6 +121,12 @@ class HttpCli(object):
try:
self.mode, self.req, self.http_ver = headerlines[0].split(" ")
# normalize incoming headers to lowercase;
# outgoing headers however are Correct-Case
for header_line in headerlines[1:]:
k, v = header_line.split(":", 1)
self.headers[k.lower()] = v.strip()
except:
msg = " ]\n#[ ".join(headerlines)
raise Pebkac(400, "bad headers:\n#[ " + msg + " ]")
@@ -137,11 +143,9 @@ class HttpCli(object):
if self.args.rsp_slp:
time.sleep(self.args.rsp_slp)
# normalize incoming headers to lowercase;
# outgoing headers however are Correct-Case
for header_line in headerlines[1:]:
k, v = header_line.split(":", 1)
self.headers[k.lower()] = v.strip()
self.ua = self.headers.get("user-agent", "")
self.is_rclone = self.ua.startswith("rclone/")
self.is_ancient = self.ua.startswith("Mozilla/4.")
v = self.headers.get("connection", "").lower()
self.keepalive = not v.startswith("close") and self.http_ver != "HTTP/1.0"
@@ -211,6 +215,14 @@ class HttpCli(object):
self.cookies = cookies
self.vpath = unquotep(vpath) # not query, so + means +
ok = "\x00" not in self.vpath
if ANYWIN:
ok = ok and not relchk(self.vpath)
if not ok:
self.log("invalid relpath [{}]".format(self.vpath))
return self.tx_404() and self.keepalive
pwd = None
ba = self.headers.get("authorization")
if ba:
@@ -233,11 +245,9 @@ class HttpCli(object):
self.dvol = self.asrv.vfs.adel[self.uname]
self.gvol = self.asrv.vfs.aget[self.uname]
if pwd and "pw" in self.ouparam and pwd != cookies.get("cppwd"):
if pwd:
self.out_headerlist.append(("Set-Cookie", self.get_pwd_cookie(pwd)[0]))
self.ua = self.headers.get("user-agent", "")
self.is_rclone = self.ua.startswith("rclone/")
if self.is_rclone:
uparam["raw"] = False
uparam["dots"] = False
@@ -274,10 +284,11 @@ class HttpCli(object):
msg = str(ex) if pex == ex else min_ex()
self.log("{}\033[0m, {}".format(msg, self.vpath), 3)
msg = "<pre>{}\r\nURL: {}\r\n".format(str(ex), self.vpath)
msg = "{}\r\nURL: {}\r\n".format(str(ex), self.vpath)
if self.hint:
msg += "hint: {}\r\n".format(self.hint)
msg = "<pre>" + html_escape(msg)
self.reply(msg.encode("utf-8", "replace"), status=pex.code, volsan=True)
return self.keepalive
except Pebkac:
@@ -344,8 +355,11 @@ class HttpCli(object):
return body
def loud_reply(self, body, *args, **kwargs):
if not kwargs.get("mime"):
kwargs["mime"] = "text/plain; charset=utf-8"
self.log(body.rstrip())
self.reply(b"<pre>" + body.encode("utf-8") + b"\r\n", *list(args), **kwargs)
self.reply(body.encode("utf-8") + b"\r\n", *list(args), **kwargs)
def urlq(self, add, rm):
"""
@@ -864,8 +878,9 @@ class HttpCli(object):
else:
# search by query params
q = body["q"]
self.log("qj: " + q)
hits, taglist = idx.search(vols, q)
n = body.get("n", self.args.srch_hits)
self.log("qj: {} |{}|".format(q, n))
hits, taglist = idx.search(vols, q, n)
msg = len(hits)
idx.p_end = time.time()
@@ -995,9 +1010,15 @@ class HttpCli(object):
pwd = self.parser.require("cppwd", 64)
self.parser.drop()
dst = "/?h"
self.out_headerlist = [
x
for x in self.out_headerlist
if x[0] != "Set-Cookie" or "cppwd=" not in x[1]
]
dst = "/"
if self.vpath:
dst = "/" + quotep(self.vpath)
dst += quotep(self.vpath)
ck, msg = self.get_pwd_cookie(pwd)
html = self.j2("msg", h1=msg, h2='<a href="' + dst + '">ack</a>', redir=dst)
@@ -1007,13 +1028,17 @@ class HttpCli(object):
def get_pwd_cookie(self, pwd):
if pwd in self.asrv.iacct:
msg = "login ok"
dur = 60 * 60 * 24 * 365
dur = int(60 * 60 * self.args.logout)
else:
msg = "naw dude"
pwd = "x" # nosec
dur = None
return [gencookie("cppwd", pwd, dur), msg]
r = gencookie("cppwd", pwd, dur)
if self.is_ancient:
r = r.rsplit(" ", 1)[0]
return [r, msg]
def handle_mkdir(self):
new_dir = self.parser.require("name", 512)
@@ -1046,6 +1071,7 @@ class HttpCli(object):
raise Pebkac(500, min_ex())
vpath = "{}/{}".format(self.vpath, sanitized).lstrip("/")
self.out_headers["X-New-Dir"] = quotep(sanitized)
self.redirect(vpath)
return True
@@ -1801,15 +1827,17 @@ class HttpCli(object):
self.redirect("", "?h#cc")
def tx_404(self, is_403=False):
rc = 404
if self.args.vague_403:
m = '<h1>404 not found &nbsp;┐( ´ -`)┌</h1><p>or maybe you don\'t have access -- try logging in or <a href="/?h">go home</a></p>'
elif is_403:
m = '<h1>403 forbiddena &nbsp;~┻━┻</h1><p>you\'ll have to log in or <a href="/?h">go home</a></p>'
rc = 403
else:
m = '<h1>404 not found &nbsp;┐( ´ -`)┌</h1><p><a href="/?h">go home</a></p>'
html = self.j2("splash", this=self, qvpath=quotep(self.vpath), msg=m)
self.reply(html.encode("utf-8"), status=404)
self.reply(html.encode("utf-8"), status=rc)
return True
def scanvol(self):
@@ -1852,7 +1880,7 @@ class HttpCli(object):
if self.args.no_stack:
raise Pebkac(403, "the stackdump feature is disabled in server config")
ret = "<pre>{}\n{}".format(time.time(), alltrace())
ret = "<pre>{}\n{}".format(time.time(), html_escape(alltrace()))
self.reply(ret.encode("utf-8"))
def tx_tree(self):
@@ -2165,7 +2193,7 @@ class HttpCli(object):
if self.can_get:
perms.append("get")
url_suf = self.urlq({}, [])
url_suf = self.urlq({}, ["k"])
is_ls = "ls" in self.uparam
is_js = self.args.force_js or self.cookies.get("js") == "y"
@@ -2226,6 +2254,7 @@ class HttpCli(object):
"srv_info": srv_info,
"dtheme": self.args.theme,
"themes": self.args.themes,
"turbolvl": self.args.turbo,
}
if not self.can_read:
if is_ls:
@@ -2384,7 +2413,7 @@ class HttpCli(object):
continue
w = r[0][:16]
q = "select k, v from mt where w = ? and k != 'x'"
q = "select k, v from mt where w = ? and +k != 'x'"
try:
for k, v in icur.execute(q, (w,)):
taglist[k] = True
@@ -2409,14 +2438,19 @@ class HttpCli(object):
if doc:
doc = unquotep(doc.replace("+", " ").split("?")[0])
j2a["docname"] = doc
doctxt = None
if next((x for x in files if x["name"] == doc), None):
with open(os.path.join(abspath, doc), "rb") as f:
doc = f.read().decode("utf-8", "replace")
docpath = os.path.join(abspath, doc)
sz = bos.path.getsize(docpath)
if sz < 1024 * self.args.txt_max:
with open(docpath, "rb") as f:
doctxt = f.read().decode("utf-8", "replace")
else:
self.log("doc 404: [{}]".format(doc), c=6)
doc = "( textfile not found )"
doctxt = "( textfile not found )"
j2a["doc"] = doc
if doctxt is not None:
j2a["doc"] = doctxt
if not self.conn.hsrv.prism:
j2a["no_prism"] = True

View File

@@ -18,6 +18,7 @@ from .httpcli import HttpCli
from .u2idx import U2idx
from .th_cli import ThumbCli
from .th_srv import HAVE_PIL, HAVE_VIPS
from .mtag import HAVE_FFMPEG
from .ico import Ico
@@ -38,7 +39,7 @@ class HttpConn(object):
self.cert_path = hsrv.cert_path
self.u2fh = hsrv.u2fh
enth = (HAVE_PIL or HAVE_VIPS) and not self.args.no_thumb
enth = (HAVE_PIL or HAVE_VIPS or HAVE_FFMPEG) and not self.args.no_thumb
self.thumbcli = ThumbCli(hsrv) if enth else None
self.ico = Ico(self.args)

View File

@@ -8,7 +8,7 @@ import shutil
import subprocess as sp
from .__init__ import PY2, WINDOWS, unicode
from .util import fsenc, fsdec, uncyg, runcmd, REKOBO_LKEY
from .util import fsenc, fsdec, uncyg, runcmd, retchk, REKOBO_LKEY
from .bos import bos
@@ -82,8 +82,9 @@ def ffprobe(abspath, timeout=10):
b"--",
fsenc(abspath),
]
rc = runcmd(cmd, timeout=timeout)
return parse_ffprobe(rc[1])
rc, so, se = runcmd(cmd, timeout=timeout)
retchk(rc, cmd, se)
return parse_ffprobe(so)
def parse_ffprobe(txt):
@@ -491,12 +492,14 @@ class MTag(object):
cmd = ["nice"] + cmd
cmd = [fsenc(x) for x in cmd]
v = sp.check_output(cmd, **args).strip()
rc, v, err = runcmd(cmd, **args)
retchk(rc, cmd, err, self.log, 5)
v = v.strip()
if not v:
continue
if "," not in tagname:
ret[tagname] = v.decode("utf-8")
ret[tagname] = v
else:
v = json.loads(v)
for tag in tagname.split(","):

View File

@@ -136,13 +136,16 @@ class SvcHub(object):
self.broker = Broker(self)
def thr_httpsrv_up(self):
time.sleep(5)
time.sleep(1 if self.args.ign_ebind_all else 5)
expected = self.broker.num_workers * self.tcpsrv.nsrv
failed = expected - self.httpsrv_up
if not failed:
return
if self.args.ign_ebind_all:
if not self.tcpsrv.srv:
for _ in range(self.broker.num_workers):
self.broker.put(False, "cb_httpsrv_up")
return
if self.args.ign_ebind and self.tcpsrv.srv:

View File

@@ -253,7 +253,9 @@ class ThumbSrv(object):
fun(abspath, tpath)
except:
msg = "{} could not create thumbnail of {}\n{}"
self.log(msg.format(fun.__name__, abspath, min_ex()), "1;30")
msg = msg.format(fun.__name__, abspath, min_ex())
c = 1 if "<Signals.SIG" in msg else "1;30"
self.log(msg, c)
with open(tpath, "wb") as _:
pass
@@ -343,6 +345,8 @@ class ThumbSrv(object):
def conv_ffmpeg(self, abspath, tpath):
ret, _ = ffprobe(abspath)
if not ret:
return
ext = abspath.rsplit(".")[-1].lower()
if ext in ["h264", "h265"] or ext in self.fmt_ffi:
@@ -417,8 +421,15 @@ class ThumbSrv(object):
m = "FFmpeg failed because it was compiled without libsox; you must set --th-ff-swr to force swr resampling:\n"
c = 1
m += "\n".join(["ff: {}".format(x) for x in serr.split("\n")])
self.log(m, c=c)
lines = serr.strip("\n").split("\n")
if len(lines) > 50:
lines = lines[:25] + ["[...]"] + lines[-25:]
txt = "\n".join(["ff: " + str(x) for x in lines])
if len(txt) > 5000:
txt = txt[:2500] + "...\nff: [...]\nff: ..." + txt[-2500:]
self.log(m + txt, c=c)
raise sp.CalledProcessError(ret, (cmd[0], b"...", cmd[-1]))
def conv_spec(self, abspath, tpath):

View File

@@ -21,6 +21,12 @@ except:
HAVE_SQLITE3 = False
try:
from pathlib import Path
except:
pass
class U2idx(object):
def __init__(self, conn):
self.log_func = conn.log_func
@@ -55,7 +61,7 @@ class U2idx(object):
uv = [wark[:16], wark]
try:
return self.run_query(vols, uq, uv, True, False)[0]
return self.run_query(vols, uq, uv, True, False, 99999)[0]
except:
raise Pebkac(500, min_ex())
@@ -76,11 +82,26 @@ class U2idx(object):
if not bos.path.exists(db_path):
return None
cur = sqlite3.connect(db_path, 2).cursor()
cur = None
if ANYWIN:
uri = ""
try:
uri = "{}?mode=ro&nolock=1".format(Path(db_path).as_uri())
cur = sqlite3.connect(uri, 2, uri=True).cursor()
self.log("ro: {}".format(db_path))
except:
self.log("could not open read-only: {}\n{}".format(uri, min_ex()))
if not cur:
# on windows, this steals the write-lock from up2k.deferred_init --
# seen on win 10.0.17763.2686, py 3.10.4, sqlite 3.37.2
cur = sqlite3.connect(db_path, 2).cursor()
self.log("opened {}".format(db_path))
self.cur[ptop] = cur
return cur
def search(self, vols, uq):
def search(self, vols, uq, lim):
"""search by query params"""
if not HAVE_SQLITE3:
return []
@@ -222,11 +243,11 @@ class U2idx(object):
q += " lower({}) {} ? ) ".format(field, oper)
try:
return self.run_query(vols, q, va, have_up, have_mt)
return self.run_query(vols, q, va, have_up, have_mt, lim)
except Exception as ex:
raise Pebkac(500, repr(ex))
def run_query(self, vols, uq, uv, have_up, have_mt):
def run_query(self, vols, uq, uv, have_up, have_mt, lim):
done_flag = []
self.active_id = "{:.6f}_{}".format(
time.time(), threading.current_thread().ident
@@ -255,7 +276,7 @@ class U2idx(object):
self.log("qs: {!r} {!r}".format(uq, uv))
ret = []
lim = int(self.args.srch_hits)
lim = min(lim, int(self.args.srch_hits))
taglist = {}
for (vtop, ptop, flags) in vols:
cur = self.get_cur(ptop)
@@ -278,7 +299,7 @@ class U2idx(object):
for hit in c:
w, ts, sz, rd, fn, ip, at = hit[:7]
lim -= 1
if lim <= 0:
if lim < 0:
break
if rd.startswith("//") or fn.startswith("//"):
@@ -307,7 +328,7 @@ class U2idx(object):
w = hit["w"]
del hit["w"]
tags = {}
q2 = "select k, v from mt where w = ? and k != 'x'"
q2 = "select k, v from mt where w = ? and +k != 'x'"
for k, v2 in cur.execute(q2, (w,)):
taglist[k] = True
tags[k] = v2

View File

@@ -95,7 +95,7 @@ class Up2k(object):
if ANYWIN:
# usually fails to set lastmod too quickly
self.lastmod_q = Queue()
self.lastmod_q = []
thr = threading.Thread(target=self._lastmodder, name="up2k-lastmod")
thr.daemon = True
thr.start()
@@ -554,12 +554,16 @@ class Up2k(object):
for d in all_vols
if d != vol and (d.vpath.startswith(vol.vpath + "/") or not vol.vpath)
]
excl += [absreal(x) for x in excl]
excl += list(self.asrv.vfs.histtab.values())
if WINDOWS:
excl = [x.replace("/", "\\") for x in excl]
excl = set(excl)
rtop = absreal(top)
n_add = n_rm = 0
try:
n_add = self._build_dir(dbw, top, set(excl), top, rei, reh, [])
n_add = self._build_dir(dbw, top, excl, top, rtop, rei, reh, [])
n_rm = self._drop_lost(dbw[0], top)
except:
m = "failed to index volume [{}]:\n{}"
@@ -572,8 +576,7 @@ class Up2k(object):
return True, n_add or n_rm or do_vac
def _build_dir(self, dbw, top, excl, cdir, rei, reh, seen):
rcdir = absreal(cdir) # a bit expensive but worth
def _build_dir(self, dbw, top, excl, cdir, rcdir, rei, reh, seen):
if rcdir in seen:
m = "bailing from symlink loop,\n prev: {}\n curr: {}\n from: {}"
self.log(m.format(seen[-1], rcdir, cdir), 3)
@@ -581,11 +584,12 @@ class Up2k(object):
seen = seen + [rcdir]
self.pp.msg = "a{} {}".format(self.pp.n, cdir)
histpath = self.asrv.vfs.histtab[top]
ret = 0
seen_files = {}
seen_files = {} # != inames; files-only for dropcheck
g = statdir(self.log_func, not self.args.no_scandir, False, cdir)
for iname, inf in sorted(g):
g = sorted(g)
inames = {x[0]: 1 for x in g}
for iname, inf in g:
abspath = os.path.join(cdir, iname)
if rei and rei.search(abspath):
continue
@@ -594,17 +598,20 @@ class Up2k(object):
lmod = int(inf.st_mtime)
sz = inf.st_size
if stat.S_ISDIR(inf.st_mode):
if abspath in excl or abspath == histpath:
rap = absreal(abspath)
if abspath in excl or rap in excl:
continue
if iname == ".th" and bos.path.isdir(os.path.join(abspath, "top")):
# abandoned or foreign, skip
continue
# self.log(" dir: {}".format(abspath))
try:
ret += self._build_dir(dbw, top, excl, abspath, rei, reh, seen)
ret += self._build_dir(dbw, top, excl, abspath, rap, rei, reh, seen)
except:
m = "failed to index subdir [{}]:\n{}"
self.log(m.format(abspath, min_ex()), c=1)
elif not stat.S_ISREG(inf.st_mode):
self.log("skip type-{:x} file [{}]".format(inf.st_mode, abspath))
else:
# self.log("file: {}".format(abspath))
seen_files[iname] = 1
@@ -612,6 +619,17 @@ class Up2k(object):
if WINDOWS:
rp = rp.replace("\\", "/").strip("/")
if rp.endswith(".PARTIAL") and time.time() - lmod < 60:
# rescan during upload
continue
if not sz and (
"{}.PARTIAL".format(iname) in inames
or ".{}.PARTIAL".format(iname) in inames
):
# placeholder for unfinished upload
continue
rd, fn = rp.rsplit("/", 1) if "/" in rp else ["", rp]
sql = "select w, mt, sz from up where rd = ? and fn = ?"
try:
@@ -781,6 +799,7 @@ class Up2k(object):
if self.mtag.prefer_mt and self.args.mtag_mt > 1:
mpool = self._start_mpool()
# TODO blocks writes to registry cursor; do chunks instead
conn = sqlite3.connect(db_path, timeout=15)
cur = conn.cursor()
c2 = conn.cursor()
@@ -806,8 +825,8 @@ class Up2k(object):
n_tags = self._tag_file(c3, *args)
else:
mpool.put(["mtag"] + args)
with self.mutex:
n_tags = len(self._flush_mpool(c3))
# not registry cursor; do not self.mutex:
n_tags = len(self._flush_mpool(c3))
n_add += n_tags
n_buf += n_tags
@@ -830,9 +849,6 @@ class Up2k(object):
cur.close()
conn.close()
with self.mutex:
gcur.connection.commit()
return n_add, n_rm, True
def _flush_mpool(self, wcur):
@@ -933,7 +949,7 @@ class Up2k(object):
n_done += 1
for w in to_delete.keys():
q = "delete from mt where w = ? and k = 't:mtp'"
q = "delete from mt where w = ? and +k = 't:mtp'"
cur.execute(q, (w,))
to_delete = {}
@@ -971,7 +987,7 @@ class Up2k(object):
with self.mutex:
done = self._flush_mpool(wcur)
for w in done:
q = "delete from mt where w = ? and k = 't:mtp'"
q = "delete from mt where w = ? and +k = 't:mtp'"
cur.execute(q, (w,))
cur.connection.commit()
@@ -1069,18 +1085,20 @@ class Up2k(object):
if parser == "mtag":
parser = self.mtag.backend
msg = "{} failed to read tags from {}:\n{}"
self.log(msg.format(parser, abspath, ex), c=3)
self._log_tag_err(parser, abspath, ex)
q.task_done()
def _log_tag_err(self, parser, abspath, ex):
msg = "{} failed to read tags from {}:\n{}".format(parser, abspath, ex)
self.log(msg.lstrip(), c=1 if "<Signals.SIG" in msg else 3)
def _tag_file(self, write_cur, entags, wark, abspath, tags=None):
if tags is None:
try:
tags = self.mtag.get(abspath)
except Exception as ex:
msg = "failed to read tags from {}:\n{}"
self.log(msg.format(abspath, ex), c=3)
self._log_tag_err("", abspath, ex)
return 0
if not bos.path.isfile(abspath):
@@ -1097,7 +1115,7 @@ class Up2k(object):
for k in tags.keys():
q = "delete from mt where w = ? and ({})".format(
" or ".join(["k = ?"] * len(tags))
" or ".join(["+k = ?"] * len(tags))
)
args = [wark[:16]] + list(tags.keys())
write_cur.execute(q, tuple(args))
@@ -1111,7 +1129,8 @@ class Up2k(object):
return ret
def _orz(self, db_path):
return sqlite3.connect(db_path, check_same_thread=False).cursor()
timeout = int(max(self.args.srch_time, 5) * 1.2)
return sqlite3.connect(db_path, timeout, check_same_thread=False).cursor()
# x.set_trace_callback(trace)
def _open_db(self, db_path):
@@ -1480,7 +1499,7 @@ class Up2k(object):
if lmod and (not linked or SYMTIME):
times = (int(time.time()), int(lmod))
if ANYWIN:
self.lastmod_q.put([dst, 0, times])
self.lastmod_q.append([dst, 0, times])
else:
bos.utime(dst, times, False)
@@ -1574,9 +1593,15 @@ class Up2k(object):
# self.log("--- " + wark + " " + dst + " finish_upload atomic " + dst, 4)
atomic_move(src, dst)
times = (int(time.time()), int(job["lmod"]))
if ANYWIN:
a = [dst, job["size"], (int(time.time()), int(job["lmod"]))]
self.lastmod_q.put(a)
a = [dst, job["size"], times]
self.lastmod_q.append(a)
elif not job["hash"]:
try:
bos.utime(dst, times)
except:
pass
a = [job[x] for x in "ptop wark prel name lmod size addr".split()]
a += [job.get("at") or time.time()]
@@ -2064,9 +2089,8 @@ class Up2k(object):
def _lastmodder(self):
while True:
ready = []
while not self.lastmod_q.empty():
ready.append(self.lastmod_q.get())
ready = self.lastmod_q
self.lastmod_q = []
# self.log("lmod: got {}".format(len(ready)))
time.sleep(5)
@@ -2171,8 +2195,7 @@ class Up2k(object):
if parsers:
tags.update(self.mtag.get_bin(parsers, abspath))
except Exception as ex:
msg = "failed to read tags from {}:\n{}"
self.log(msg.format(abspath, ex), c=3)
self._log_tag_err("", abspath, ex)
continue
with self.mutex:

View File

@@ -9,6 +9,7 @@ import time
import base64
import select
import struct
import signal
import hashlib
import platform
import traceback
@@ -912,6 +913,9 @@ def sanitize_fn(fn, ok, bad):
if "/" not in ok:
fn = fn.replace("\\", "/").split("/")[-1]
if fn.lower() in bad:
fn = "_" + fn
if ANYWIN:
remap = [
["<", ""],
@@ -927,16 +931,26 @@ def sanitize_fn(fn, ok, bad):
for a, b in [x for x in remap if x[0] not in ok]:
fn = fn.replace(a, b)
bad.extend(["con", "prn", "aux", "nul"])
bad = ["con", "prn", "aux", "nul"]
for n in range(1, 10):
bad += "com{0} lpt{0}".format(n).split(" ")
if fn.lower() in bad:
fn = "_" + fn
if fn.lower().split(".")[0] in bad:
fn = "_" + fn
return fn.strip()
def relchk(rp):
if ANYWIN:
if "\n" in rp or "\r" in rp:
return "x\nx"
p = re.sub(r'[\\:*?"<>|]', "", rp)
if p != rp:
return "[{}]".format(p)
def absreal(fpath):
try:
return fsdec(os.path.abspath(os.path.realpath(fsenc(fpath))))
@@ -1337,8 +1351,8 @@ def guess_mime(url, fallback="application/octet-stream"):
return ret
def runcmd(argv, timeout=None):
p = sp.Popen(argv, stdout=sp.PIPE, stderr=sp.PIPE)
def runcmd(argv, timeout=None, **ka):
p = sp.Popen(argv, stdout=sp.PIPE, stderr=sp.PIPE, **ka)
if not timeout or PY2:
stdout, stderr = p.communicate()
else:
@@ -1353,9 +1367,10 @@ def runcmd(argv, timeout=None):
return [p.returncode, stdout, stderr]
def chkcmd(argv):
ok, sout, serr = runcmd(argv)
def chkcmd(argv, **ka):
ok, sout, serr = runcmd(argv, **ka)
if ok != 0:
retchk(ok, argv, serr)
raise Exception(serr)
return sout, serr
@@ -1372,6 +1387,46 @@ def mchkcmd(argv, timeout=10):
raise sp.CalledProcessError(rv, (argv[0], b"...", argv[-1]))
def retchk(rc, cmd, serr, logger=None, color=None):
if rc < 0:
rc = 128 - rc
if rc < 126:
return
s = None
if rc > 128:
try:
s = str(signal.Signals(rc - 128))
except:
pass
elif rc == 126:
s = "invalid program"
elif rc == 127:
s = "program not found"
else:
s = "invalid retcode"
if s:
m = "{} <{}>".format(rc, s)
else:
m = str(rc)
try:
c = " ".join([fsdec(x) for x in cmd])
except:
c = str(cmd)
m = "error {} from [{}]".format(m, c)
if serr:
m += "\n" + serr
if logger:
logger(m, color)
else:
raise Exception(m)
def gzip_orig_sz(fn):
with open(fsenc(fn), "rb") as f:
f.seek(-4, 2)

File diff suppressed because it is too large Load Diff

View File

@@ -146,12 +146,13 @@
have_del = {{ have_del|tojson }},
have_unpost = {{ have_unpost|tojson }},
have_zip = {{ have_zip|tojson }},
turbolvl = {{ turbolvl|tojson }},
txt_ext = "{{ txt_ext }}",
{% if no_prism %}no_prism = 1,{% endif %}
readme = {{ readme|tojson }},
ls0 = {{ ls0|tojson }};
document.documentElement.setAttribute("class", localStorage.theme || dtheme);
document.documentElement.className = localStorage.theme || dtheme;
</script>
<script src="/.cpr/util.js?_={{ ts }}"></script>
<script src="/.cpr/baguettebox.js?_={{ ts }}"></script>

View File

@@ -3,6 +3,7 @@
function dbg(msg) {
ebi('path').innerHTML = msg;
}
var XHR = XMLHttpRequest;
// toolbar
@@ -128,8 +129,8 @@ ebi('op_up2k').innerHTML = (
var o = mknod('div');
o.innerHTML = (
'<div id="drops">\n' +
' <div class="dropdesc" id="up_zd"><div>🚀 Upload<br /><span></span><div>🚀</div><div>🚀</div></div></div>\n' +
' <div class="dropdesc" id="srch_zd"><div>🔎 Search<br /><span></span><div>🔎</div><div>🔎</div></div></div>\n' +
' <div class="dropdesc" id="up_zd"><div>🚀 Upload<br /><span></span><div>🚀<b>Upload</b></div><div><b>Upload</b>🚀</div></div></div>\n' +
' <div class="dropdesc" id="srch_zd"><div>🔎 Search<br /><span></span><div>🔎<b>Search</b></div><div><b>Search</b>🔎</div></div></div>\n' +
' <div class="dropzone" id="up_dz" v="up_zd"></div>\n' +
' <div class="dropzone" id="srch_dz" v="srch_zd"></div>\n' +
'</div>'
@@ -148,7 +149,6 @@ ebi('op_cfg').innerHTML = (
' <a id="thumbs" class="tgl btn" href="#" tt="in icon view, toggle icons or thumbnails$NHotkey: T">🖼️ thumbs</a>\n' +
' <a id="dotfiles" class="tgl btn" href="#" tt="show hidden files (if server permits)">dotfiles</a>\n' +
' <a id="ireadme" class="tgl btn" href="#" tt="show README.md in folder listings">📜 readme</a>\n' +
' <a id="spafiles" class="tgl btn" href="#" tt="speedboost when not using the navpane;$Nturn it off if things arent loading somehow">spa</a>\n' +
' </div>\n' +
'</div>\n' +
'<div>\n' +
@@ -164,7 +164,7 @@ ebi('op_cfg').innerHTML = (
' <div>\n' +
' <a id="u2turbo" class="tgl btn ttb" href="#" tt="the yolo button, you probably DO NOT want to enable this:$N$Nuse this if you were uploading a huge amount of files and had to restart for some reason, and want to continue the upload ASAP$N$Nthis replaces the hash-check with a simple <em>&quot;does this have the same filesize on the server?&quot;</em> so if the file contents are different it will NOT be uploaded$N$Nyou should turn this off when the upload is done, and then &quot;upload&quot; the same files again to let the client verify them">turbo</a>\n' +
' <a id="u2tdate" class="tgl btn ttb" href="#" tt="has no effect unless the turbo button is enabled$N$Nreduces the yolo factor by a tiny amount; checks whether the file timestamps on the server matches yours$N$Nshould <em>theoretically</em> catch most unfinished/corrupted uploads, but is not a substitute for doing a verification pass with turbo disabled afterwards">date-chk</a>\n' +
' <a id="flag_en" class="tgl btn" href="#" tt="ensure only one tab is uploading at a time $N (other tabs must have this enabled too)">💤</a>\n' +
' <a id="flag_en" class="tgl btn" href="#" tt="ensure only one tab is uploading at a time $N -- other tabs must have this enabled too $N -- only affects tabs on the same domain">💤</a>\n' +
' </td>\n' +
' </div>\n' +
'</div>\n' +
@@ -319,8 +319,8 @@ var mpl = (function () {
ebi('op_player').innerHTML = (
'<div><h3>switches</h3><div>' +
'<a href="#" class="tgl btn" id="au_preload" tt="start loading the next song near the end for gapless playback">preload</a>' +
'<a href="#" class="tgl btn" id="au_fullpre" tt="try to preload the entire song;$N✔️ enable on <b>unreliable</b> connections,$N❌ <b>disable</b> on slow connections probably">full</a>' +
'<a href="#" class="tgl btn" id="au_npclip" tt="show buttons for clipboarding the currently playing song">/np clip</a>' +
'<a href="#" class="tgl btn" id="au_fullpre" tt="try to preload the entire song;$N enable on <b>unreliable</b> connections,$N❌ <b>disable</b> on slow connections probably">full</a>' +
'<a href="#" class="tgl btn" id="au_npclip" tt="show buttons for clipboarding the currently playing song">/np</a>' +
'<a href="#" class="tgl btn" id="au_os_ctl" tt="os integration (media hotkeys / osd)">os-ctl</a>' +
'<a href="#" class="tgl btn" id="au_os_seek" tt="allow seeking through os integration">seek</a>' +
'<a href="#" class="tgl btn" id="au_osd_cv" tt="show album cover in osd">art</a>' +
@@ -452,7 +452,7 @@ var mpl = (function () {
cover = null;
for (var a = 0, aa = files.length; a < aa; a++) {
if (/^(cover|folder)\.(jpe?g|png|gif)$/.test(files[a].textContent)) {
if (/^(cover|folder)\.(jpe?g|png|gif)$/i.test(files[a].textContent)) {
cover = noq_href(files[a]);
break;
}
@@ -612,6 +612,10 @@ function MPlayer() {
r.fvol = 0;
r.au.pause();
mpl.pp();
var t = mp.au.currentTime - 0.8;
if (isFinite(t))
mp.au.currentTime = Math.max(t, 0);
}
else if (r.fvol > r.vol)
r.fvol = r.vol;
@@ -666,7 +670,7 @@ function ft2dict(tr) {
for (var a = 1, aa = th.length; a < aa; a++) {
var tv = tr.cells[a].textContent,
tk = a == 1 ? 'file' : th[a].getAttribute('name').split('/').pop(),
tk = a == 1 ? 'file' : th[a].getAttribute('name').split('/').pop().toLowerCase(),
vis = th[a].className.indexOf('min') === -1;
if (!tv)
@@ -1099,6 +1103,7 @@ var mpui = (function () {
fpreloaded = null;
r.progress_updater = function () {
//console.trace();
timer.add(updater_impl, true);
};
@@ -1414,7 +1419,7 @@ var audio_eq = (function () {
// plays the tid'th audio file on the page
function play(tid, is_ev, seek, call_depth) {
function play(tid, is_ev, seek) {
if (mp.order.length == 0)
return console.log('no audio found wait what');
@@ -1462,6 +1467,7 @@ function play(tid, is_ev, seek, call_depth) {
mp.au2 = new Audio();
mp.au.onerror = evau_error;
mp.au.onprogress = pbar.drawpos;
mp.au.onplaying = mpui.progress_updater;
mp.au.onended = next_song;
widget.open();
}
@@ -1478,6 +1484,7 @@ function play(tid, is_ev, seek, call_depth) {
t.onerror = t.onprogress = t.onended = null;
mp.au.onerror = evau_error;
mp.au.onprogress = pbar.drawpos;
mp.au.onplaying = mpui.progress_updater;
mp.au.onended = next_song;
}
else
@@ -1562,12 +1569,37 @@ function evau_error(e) {
err = 'Unknown Errol';
break;
}
if (eplaya.error.message)
err += '\n\n' + eplaya.error.message;
var em = '' + eplaya.error.message,
mfile = '\n\nFile: «' + uricom_dec(eplaya.src.split('/').pop())[0] + '»',
e404 = 'Could not play audio; error 404: File not found.',
e403 = 'Could not play audio; error 403: Access denied.\n\nTry pressing F5 to reload, maybe you got logged out';
err += '\n\nFile: «' + uricom_dec(eplaya.src.split('/').pop())[0] + '»';
if (em)
err += '\n\n' + em;
toast.warn(15, esc(basenames(err)));
if (em.startsWith('403: '))
err = e403;
if (em.startsWith('404: '))
err = e404;
toast.warn(15, esc(basenames(err + mfile)));
if (em.startsWith('MEDIA_ELEMENT_ERROR:')) {
// chromish for 40x
var xhr = new XHR();
xhr.open('HEAD', eplaya.src, true);
xhr.onreadystatechange = function () {
if (this.readyState != XHR.DONE || this.status < 400)
return;
err = this.status == 403 ? e403 : this.status == 404 ? e404 :
'Could not play audio; server error ' + this.status;
toast.warn(15, esc(basenames(err + mfile)));
};
xhr.send();
}
}
@@ -1913,9 +1945,6 @@ var fileman = (function () {
if (!md.hasOwnProperty(k))
continue;
md[k.toLowerCase()] = md[k];
k = k.toLowerCase();
if (k.startsWith('.'))
md[k.slice(1)] = md[k];
}
@@ -2150,7 +2179,7 @@ var fileman = (function () {
var dst = base + uricom_enc(f[0].inew.value, false);
function rename_cb() {
if (this.readyState != XMLHttpRequest.DONE)
if (this.readyState != XHR.DONE)
return;
if (this.status !== 200) {
@@ -2163,7 +2192,7 @@ var fileman = (function () {
return rn_apply();
}
var xhr = new XMLHttpRequest();
var xhr = new XHR();
xhr.open('GET', f[0].src + '?move=' + dst, true);
xhr.onreadystatechange = rename_cb;
xhr.send();
@@ -2185,7 +2214,7 @@ var fileman = (function () {
return toast.err(3, 'select at least 1 item to delete');
function deleter() {
var xhr = new XMLHttpRequest(),
var xhr = new XHR(),
vp = vps.shift();
if (!vp) {
@@ -2200,7 +2229,7 @@ var fileman = (function () {
xhr.send();
}
function delete_cb() {
if (this.readyState != XMLHttpRequest.DONE)
if (this.readyState != XHR.DONE)
return;
if (this.status !== 200) {
@@ -2293,7 +2322,7 @@ var fileman = (function () {
return;
function paster() {
var xhr = new XMLHttpRequest(),
var xhr = new XHR(),
vp = req.shift();
if (!vp) {
@@ -2311,7 +2340,7 @@ var fileman = (function () {
xhr.send();
}
function paste_cb() {
if (this.readyState != XMLHttpRequest.DONE)
if (this.readyState != XHR.DONE)
return;
if (this.status !== 200) {
@@ -2412,6 +2441,14 @@ var showfile = (function () {
var em = QS('#bdoc>pre');
if (em)
em = [r.sname(window.location.search), window.location.hash, em.textContent];
else {
var m = /[?&]doc=([^&]+)/.exec(window.location.search);
if (m) {
setTimeout(function () {
r.show(uricom_dec(m[1])[0], true);
}, 1);
}
}
r.setstyle = function () {
if (window['no_prism'])
@@ -2464,7 +2501,7 @@ var showfile = (function () {
};
r.show = function (url, no_push) {
var xhr = new XMLHttpRequest();
var xhr = new XHR();
xhr.url = url;
xhr.no_push = no_push;
xhr.ts = Date.now();
@@ -2474,13 +2511,11 @@ var showfile = (function () {
};
function load_cb() {
if (this.readyState != XMLHttpRequest.DONE)
if (this.readyState != XHR.DONE)
return;
if (this.status !== 200) {
toast.err(0, "recvtree, http " + this.status + ": " + this.responseText);
if (!xhrchk(this, "could not load textfile:\n\nerror ", "404, file not found"))
return;
}
render([this.url, '', this.responseText], this.no_push);
}
@@ -2508,7 +2543,7 @@ var showfile = (function () {
el = el || QS('#doc>code');
Prism.highlightElement(el);
if (el.getAttribute('class') == 'language-ans')
if (el.className == 'language-ans')
r.ansify(el);
}
catch (ex) { }
@@ -2529,7 +2564,7 @@ var showfile = (function () {
el.textContent = txt;
el.innerHTML = '<code>' + el.innerHTML + '</code>';
if (!window['no_prism']) {
el.setAttribute('class', 'prism linkable-line-numbers line-numbers language-' + lang);
el.className = 'prism linkable-line-numbers line-numbers language-' + lang;
if (!defer)
fun(el.firstChild);
else
@@ -2659,7 +2694,7 @@ var showfile = (function () {
};
var bdoc = ebi('bdoc');
bdoc.setAttribute('class', 'line-numbers');
bdoc.className = 'line-numbers';
bdoc.innerHTML = (
'<div id="hdoc" class="ghead">\n' +
'<a href="#" class="btn" id="xdoc" tt="return to folder view$NHotkey: M">❌ close</a>\n' +
@@ -2827,7 +2862,7 @@ var thegrid = (function () {
else if (in_tree && !have_sel)
in_tree.click();
else if (is_dir && !have_sel && treectl.spa)
else if (is_dir && !have_sel)
treectl.reqls(href, true, true);
else if (!is_img && have_sel)
@@ -2854,19 +2889,24 @@ var thegrid = (function () {
for (var a = 0, aa = ths.length; a < aa; a++) {
var tr = ebi(ths[a].getAttribute('ref')).closest('tr'),
cl = tr.getAttribute('class') || '';
cl = tr.className || '';
if (noq_href(ths[a]).endsWith('/'))
cl += ' dir';
ths[a].setAttribute('class', cl);
ths[a].className = cl;
}
var uns = QS('#ggrid a[ref="unsearch"]');
if (uns)
uns.onclick = function (e) {
ev(e);
ebi('unsearch').click();
};
var sp = ['unsearch', 'moar'];
for (var a = 0; a < sp.length; a++)
(function (a) {
var o = QS('#ggrid a[ref="' + sp[a] + '"]');
if (o)
o.onclick = function (e) {
ev(e);
ebi(sp[a]).click();
};
})(a);
};
r.tippen = function () {
@@ -3284,7 +3324,8 @@ document.onkeydown = function (e) {
var trs = [],
orig_url = null,
orig_html = null;
orig_html = null,
cap = 125;
for (var a = 0; a < sconf.length; a++) {
var html = ['<tr><td><br />' + sconf[a][0] + '</td>'];
@@ -3313,12 +3354,13 @@ document.onkeydown = function (e) {
var o = QSA('#op_search input');
for (var a = 0; a < o.length; a++) {
o[a].oninput = ev_search_input;
o[a].onkeydown = ev_search_keydown;
}
function srch_msg(err, txt) {
var o = ebi('srch_q');
o.textContent = txt;
o.style.color = err ? '#f09' : '#c90';
clmod(o, 'err', err);
}
var search_timeout,
@@ -3338,12 +3380,18 @@ document.onkeydown = function (e) {
encode_query();
set_vq();
cap = 125;
clearTimeout(defer_timeout);
defer_timeout = setTimeout(try_search, 2000);
try_search(v);
}
function ev_search_keydown(e) {
if (e.key == 'Enter')
do_search();
}
function try_search(v) {
if (Date.now() - search_in_progress > 30 * 1000) {
clearTimeout(defer_timeout);
@@ -3372,8 +3420,6 @@ document.onkeydown = function (e) {
vs = ebi('srch_' + k + 'v').value,
tvs = [];
if (k == 'name')
console.log('a');
while (vs) {
vs = vs.trim();
if (!vs)
@@ -3456,17 +3502,17 @@ document.onkeydown = function (e) {
srch_msg(false, "searching...");
clearTimeout(search_timeout);
var xhr = new XMLHttpRequest();
var xhr = new XHR();
xhr.open('POST', '/?srch', true);
xhr.setRequestHeader('Content-Type', 'text/plain');
xhr.onreadystatechange = xhr_search_results;
xhr.ts = Date.now();
xhr.q_raw = ebi('q_raw').value;
xhr.send(JSON.stringify({ "q": xhr.q_raw }));
xhr.send(JSON.stringify({ "q": xhr.q_raw, "n": cap }));
}
function xhr_search_results() {
if (this.readyState != XMLHttpRequest.DONE)
if (this.readyState != XHR.DONE)
return;
if (this.status !== 200) {
@@ -3495,7 +3541,9 @@ document.onkeydown = function (e) {
var html = mk_files_header(tagord), seen = {};
html.push('<tbody>');
html.push('<tr><td>-</td><td colspan="42"><a href="#" id="unsearch"><big style="font-weight:bold">[❌] close search results</big></a></td></tr>');
html.push('<tr class="srch_hdr"><td>-</td><td><a href="#" id="unsearch"><big style="font-weight:bold">[❌] close search results</big></a> -- showing ' +
res.hits.length + ' hits' + (res.hits.length == cap ? ' -- <a href="#" id="moar">load more</a>' : '') + '</td></tr>');
for (var a = 0; a < res.hits.length; a++) {
var r = res.hits[a],
ts = parseInt(r.ts),
@@ -3547,6 +3595,9 @@ document.onkeydown = function (e) {
sethash('q=' + uricom_enc(this.q_raw));
ebi('unsearch').onclick = unsearch;
var m = ebi('moar');
if (m)
m.onclick = moar;
}
function unsearch(e) {
@@ -3558,6 +3609,12 @@ document.onkeydown = function (e) {
sethash('');
reload_browser();
}
function moar(e) {
ev(e);
cap *= 2;
do_search();
}
})();
@@ -3575,7 +3632,6 @@ var treectl = (function () {
mentered = null,
treesz = clamp(icfg_get('treesz', 16), 10, 50);
bcfg_bind(r, 'spa', 'spafiles', true);
bcfg_bind(r, 'ireadme', 'ireadme', true);
bcfg_bind(r, 'dyn', 'dyntree', true, onresize);
bcfg_bind(r, 'dots', 'dotfiles', false, function (v) {
@@ -3783,7 +3839,7 @@ var treectl = (function () {
};
function get_tree(top, dst, rst) {
var xhr = new XMLHttpRequest();
var xhr = new XHR();
xhr.top = top;
xhr.dst = dst;
xhr.rst = rst;
@@ -3795,13 +3851,11 @@ var treectl = (function () {
}
function recvtree() {
if (this.readyState != XMLHttpRequest.DONE)
if (this.readyState != XHR.DONE)
return;
if (this.status !== 200) {
toast.err(0, "recvtree, http " + this.status + ": " + this.responseText);
if (!xhrchk(this, "could not list subfolders:\n\nerror ", "404, folder not found"))
return;
}
var cur = ebi('treeul').getAttribute('ts');
if (cur && parseInt(cur) > this.ts) {
@@ -3844,7 +3898,6 @@ var treectl = (function () {
QS('#treeul>li>a+a').textContent = '[root]';
despin('#tree');
reload_tree();
onresize();
var fun = r.dir_cb;
if (fun) {
@@ -3876,7 +3929,7 @@ var treectl = (function () {
cl = 'par';
}
links[a].setAttribute('class', cl);
links[a].className = cl;
links[a].onclick = treego;
links[a].onmouseenter = nowrap ? menter : null;
links[a].onmouseleave = nowrap ? mleave : null;
@@ -3897,6 +3950,7 @@ var treectl = (function () {
catch (ex) { }
r.pdir.shift();
r.pdirw = -1;
onresize();
}
function compy() {
@@ -3943,7 +3997,7 @@ var treectl = (function () {
return true;
ev(e);
if (this.getAttribute('class') == 'hl' &&
if (this.className == 'hl' &&
this.previousSibling.textContent == '-') {
treegrow.call(this.previousSibling, e);
return;
@@ -3954,7 +4008,7 @@ var treectl = (function () {
}
r.reqls = function (url, hpush, no_tree) {
var xhr = new XMLHttpRequest();
var xhr = new XHR();
xhr.top = url;
xhr.hpush = hpush;
xhr.ts = Date.now();
@@ -3983,13 +4037,11 @@ var treectl = (function () {
}
function recvls() {
if (this.readyState != XMLHttpRequest.DONE)
if (this.readyState != XHR.DONE)
return;
if (this.status !== 200) {
toast.err(0, "recvls, http " + this.status + ": " + this.responseText);
if (!xhrchk(this, "could not list files in folder:\n\nerror ", "404, folder not found"))
return;
}
var cur = ebi('files').getAttribute('ts');
if (cur && parseInt(cur) > this.ts) {
@@ -4029,7 +4081,6 @@ var treectl = (function () {
r.ls_cb = null;
fun();
}
eval_hash();
}
r.gentab = function (top, res) {
@@ -4094,6 +4145,7 @@ var treectl = (function () {
apply_perms(res.perms);
fileman.render();
}
setTimeout(eval_hash, 1);
}
var m = scan_hash(hash0),
@@ -4118,10 +4170,11 @@ var treectl = (function () {
r.hydrate = function () {
qsr('#bbsw');
if (ls0 === null) {
var xhr = new XMLHttpRequest();
var xhr = new XHR();
xhr.open('GET', '/?am_js', true);
xhr.send();
r.ls_cb = showfile.addlinks;
return r.reqls(get_evpath(), false, true);
}
@@ -4209,7 +4262,7 @@ var treectl = (function () {
function enspin(sel) {
despin(sel);
var d = mknod('div');
d.setAttribute('class', 'dumb_loader_thing');
d.className = 'dumb_loader_thing';
d.innerHTML = '🌲';
var tgt = QS(sel);
tgt.insertBefore(d, tgt.childNodes[0]);
@@ -4315,7 +4368,7 @@ function find_file_col(txt) {
for (var a = 0; a < tds.length; a++) {
var spans = tds[a].getElementsByTagName('span');
if (spans.length && spans[0].textContent == txt) {
min = (tds[a].getAttribute('class') || '').indexOf('min') !== -1;
min = (tds[a].className || '').indexOf('min') !== -1;
i = a;
break;
}
@@ -4461,7 +4514,7 @@ var filecols = (function () {
tds = QSA('#files>tbody>tr>td:nth-child(' + (a + 1) + ')');
for (var b = 0, bb = tds.length; b < bb; b++)
tds[b].setAttribute('class', cls);
tds[b].className = cls;
}
if (window['tt']) {
tt.att(ebi('hcols'));
@@ -4623,18 +4676,21 @@ var settheme = (function () {
light = !!(theme.indexOf('y') + 1);
function freshen() {
var cl = document.documentElement.getAttribute('class');
var cl = document.documentElement.className;
cl = cl.replace(/\b(light|dark|[a-z]{1,2})\b/g, '').replace(/ +/g, ' ');
document.documentElement.setAttribute('class', cl + ' ' + theme + ' ');
document.documentElement.className = cl + ' ' + theme + ' ';
pbar.drawbuf();
pbar.drawpos();
vbar.draw();
showfile.setstyle();
var html = [], itheme = ax.indexOf(theme.charAt(0)) * 2 + (light ? 1 : 0);
var html = [], itheme = ax.indexOf(theme.charAt(0)) * 2 + (light ? 1 : 0),
names = ['classic dark', 'classic light', 'flat dark', 'flat light', 'vice', 'hotdog stand'];
for (var a = 0; a < themes; a++)
html.push('<a href="#" class="btn tgl' + (a == itheme ? ' on' : '') + '">' + a + '</a>');
html.push('<a href="#" class="btn tgl' + (a == itheme ? ' on' : '') +
'" tt="' + (names[a] || 'custom') + '">' + a + '</a>');
ebi('themes').innerHTML = html.join('');
var btns = QSA('#themes a');
@@ -4642,6 +4698,7 @@ var settheme = (function () {
btns[a].onclick = settheme;
bcfg_set('light', light);
tt.att(ebi('themes'));
}
function settheme(e) {
@@ -4759,6 +4816,7 @@ var msel = (function () {
r.all = [];
var links = QSA('#files tbody td:nth-child(2) a:last-child'),
is_srch = !!ebi('unsearch'),
vbase = get_evpath();
for (var a = 0, aa = links.length; a < aa; a++) {
@@ -4773,7 +4831,8 @@ var msel = (function () {
if (item.sel)
r.sel.push(item);
links[a].closest('tr').setAttribute('tabindex', '0');
if (!is_srch)
links[a].closest('tr').setAttribute('tabindex', '0');
}
};
@@ -4842,14 +4901,17 @@ var msel = (function () {
frm.submit();
};
r.render = function () {
var tds = QSA('#files tbody td+td+td');
for (var a = 0, aa = tds.length; a < aa; a++) {
tds[a].onclick = r.seltgl;
}
var tds = QSA('#files tbody td+td+td'),
is_srch = !!ebi('unsearch');
if (!is_srch)
for (var a = 0, aa = tds.length; a < aa; a++)
tds[a].onclick = r.seltgl;
r.selui(true);
arcfmt.render();
fileman.render();
ebi('selzip').style.display = ebi('unsearch') ? 'none' : '';
ebi('selzip').style.display = is_srch ? 'none' : '';
}
return r;
})();
@@ -4875,7 +4937,7 @@ var msel = (function () {
fd.append("act", "mkdir");
fd.append("name", tb.value);
var xhr = new XMLHttpRequest();
var xhr = new XHR();
xhr.vp = get_evpath();
xhr.dn = tb.value;
xhr.open('POST', xhr.vp, true);
@@ -4887,7 +4949,7 @@ var msel = (function () {
};
function cb() {
if (this.readyState != XMLHttpRequest.DONE)
if (this.readyState != XHR.DONE)
return;
if (this.vp !== get_evpath()) {
@@ -4895,6 +4957,8 @@ var msel = (function () {
return;
}
xhrchk(this, "could not create subfolder:\n\nerror ", "404, parent folder not found");
if (this.status !== 200) {
sf.textContent = 'error: ' + this.responseText;
return;
@@ -4903,7 +4967,10 @@ var msel = (function () {
tb.value = '';
clmod(sf, 'vis');
sf.textContent = '';
treectl.goto(this.vp + uricom_enc(this.dn) + '/', true);
var dn = this.getResponseHeader('X-New-Dir');
dn = dn || uricom_enc(this.dn);
treectl.goto(this.vp + dn + '/', true);
}
})();
@@ -4921,7 +4988,7 @@ var msel = (function () {
clmod(sf, 'vis', 1);
sf.textContent = 'sending...';
var xhr = new XMLHttpRequest(),
var xhr = new XHR(),
ct = 'application/x-www-form-urlencoded;charset=UTF-8';
xhr.msg = tb.value;
@@ -4937,9 +5004,11 @@ var msel = (function () {
};
function cb() {
if (this.readyState != XMLHttpRequest.DONE)
if (this.readyState != XHR.DONE)
return;
xhrchk(this, "could not send message:\n\nerror ", "404, parent folder not found");
if (this.status !== 200) {
sf.textContent = 'error: ' + this.responseText;
return;
@@ -5054,15 +5123,11 @@ var unpost = (function () {
html = [];
function unpost_load_cb() {
if (this.readyState != XMLHttpRequest.DONE)
if (this.readyState != XHR.DONE)
return;
if (this.status !== 200) {
var msg = this.responseText;
toast.err(9, 'unpost-load failed:\n' + msg);
ebi('op_unpost').innerHTML = html.join('\n');
return;
}
if (!xhrchk(this, "unpost-load failed:\n\nerror ", "404, file not found??"))
return ebi('op_unpost').innerHTML = 'failed to load unpost list from server';
var res = JSON.parse(this.responseText);
if (res.length) {
@@ -5075,7 +5140,7 @@ var unpost = (function () {
html.push("<table><thead><tr><td></td><td>time</td><td>size</td><td>file</td></tr></thead><tbody>");
}
else
html.push("<p>sike! no uploads " + (filt.value ? 'matching that filter' : '') + " are sufficiently recent</p>");
html.push("sike! no uploads " + (filt.value ? 'matching that filter' : '') + " are sufficiently recent");
var mods = [1000, 100, 10];
for (var a = 0; a < res.length; a++) {
@@ -5102,7 +5167,7 @@ var unpost = (function () {
if (filt.value)
q += '&filter=' + uricom_enc(filt.value, true);
var xhr = new XMLHttpRequest();
var xhr = new XHR();
xhr.open('GET', q, true);
xhr.onreadystatechange = unpost_load_cb;
xhr.send();
@@ -5111,7 +5176,7 @@ var unpost = (function () {
};
function unpost_delete_cb() {
if (this.readyState != XMLHttpRequest.DONE)
if (this.readyState != XHR.DONE)
return;
if (this.status !== 200) {
@@ -5162,7 +5227,7 @@ var unpost = (function () {
toast.inf(0, "deleting " + req.length + " files...");
var xhr = new XMLHttpRequest();
var xhr = new XHR();
xhr.n = n;
xhr.n2 = n2;
xhr.open('POST', '/?delete', true);
@@ -5203,7 +5268,7 @@ function wintitle(txt) {
ebi('path').onclick = function (e) {
var a = e.target.closest('a[href]');
if (!treectl.spa || !a || !(a = a.getAttribute('href') + '') || !a.endsWith('/'))
if (!a || !(a = a.getAttribute('href') + '') || !a.endsWith('/'))
return;
thegrid.setvis(true);
@@ -5223,11 +5288,8 @@ ebi('files').onclick = ebi('docul').onclick = function (e) {
el.click();
return ev(e);
}
if (treectl.spa) {
treectl.reqls(tgt.getAttribute('href'), true, true);
return ev(e);
}
return;
treectl.reqls(tgt.getAttribute('href'), true, true);
return ev(e);
}
tgt = e.target.closest('a[hl]');

View File

@@ -45,7 +45,9 @@
<tr><td></td><td><a href="../{{ url_suf }}">parent folder</a></td><td>-</td><td>-</td></tr>
{%- for f in files %}
<tr><td>{{ f.lead }}</td><td><a href="{{ f.href }}{{ url_suf }}">{{ f.name|e }}</a></td><td>{{ f.sz }}</td><td>{{ f.dt }}</td></tr>
<tr><td>{{ f.lead }}</td><td><a href="{{ f.href }}{{
'&' + url_suf[1:] if url_suf[:1] == '?' and '?' in f.href else url_suf
}}">{{ f.name|e }}</a></td><td>{{ f.sz }}</td><td>{{ f.dt }}</td></tr>
{%- endfor %}
</tbody>

View File

@@ -161,7 +161,7 @@ blink {
height: 1.05em;
margin: -.2em .3em -.2em -.4em;
display: inline-block;
border: 1px solid rgba(0,0,0,0.2);
border: 1px solid rgba(154,154,154,0.6);
border-width: .2em .2em 0 0;
transform: rotate(45deg);
}
@@ -236,9 +236,6 @@ blink {
html.z #toc li {
border-width: 0;
}
html.z #mn a:not(:last-child)::after {
border-color: rgba(255,255,255,0.3);
}
html.z #mn a {
color: #ccc;
}

View File

@@ -140,7 +140,7 @@ var md_opt = {
btn = document.getElementById("lightswitch"),
f = function (e) {
if (e) { e.preventDefault(); drk = !drk; }
document.documentElement.setAttribute("class", drk? "z":"y");
document.documentElement.className = drk? "z":"y";
btn.innerHTML = "go " + (drk ? "light":"dark");
l.light = drk? 0:1;
};

View File

@@ -278,7 +278,7 @@ function convert_markdown(md_text, dest_dom) {
if (!txt)
nodes[a].textContent = href;
else if (href !== txt)
nodes[a].setAttribute('class', 'vis');
nodes[a].className = 'vis';
}
// todo-lists (should probably be a marked extension)
@@ -294,7 +294,7 @@ function convert_markdown(md_text, dest_dom) {
var clas = done ? 'done' : 'pend';
var char = done ? 'Y' : 'N';
dom_li.setAttribute('class', 'task-list-item');
dom_li.className = 'task-list-item';
dom_li.style.listStyleType = 'none';
var html = dom_li.innerHTML;
dom_li.innerHTML =
@@ -468,11 +468,11 @@ function init_toc() {
for (var a = 0; a < anchors.length; a++) {
if (anchors[a].active) {
anchors[a].active = false;
links[a].setAttribute('class', '');
links[a].className = '';
}
}
anchors[hit].active = true;
links[hit].setAttribute('class', 'act');
links[hit].className = 'act';
}
var pane_height = parseInt(getComputedStyle(dom_toc).height);

View File

@@ -144,16 +144,16 @@ redraw = (function () {
map_pre = genmap(dom_pre, map_pre);
}
function setsbs() {
dom_wrap.setAttribute('class', '');
dom_swrap.setAttribute('class', '');
dom_wrap.className = '';
dom_swrap.className = '';
onresize();
}
function modetoggle() {
var mode = dom_nsbs.innerHTML;
dom_nsbs.innerHTML = mode == 'editor' ? 'preview' : 'editor';
mode += ' single';
dom_wrap.setAttribute('class', mode);
dom_swrap.setAttribute('class', mode);
dom_wrap.className = mode;
dom_swrap.className = mode;
onresize();
}
@@ -255,7 +255,7 @@ function Modpoll() {
console.log('modpoll...');
var url = (document.location + '').split('?')[0] + '?raw&_=' + Date.now();
var xhr = new XMLHttpRequest();
var xhr = new XHR();
xhr.open('GET', url, true);
xhr.responseType = 'text';
xhr.onreadystatechange = r.cb;
@@ -268,7 +268,7 @@ function Modpoll() {
return;
}
if (this.readyState != XMLHttpRequest.DONE)
if (this.readyState != XHR.DONE)
return;
if (this.status !== 200) {
@@ -309,7 +309,7 @@ var modpoll = new Modpoll();
window.onbeforeunload = function (e) {
if ((ebi("save").getAttribute('class') + '').indexOf('disabled') >= 0)
if ((ebi("save").className + '').indexOf('disabled') >= 0)
return; //nice (todo)
e.preventDefault(); //ff
@@ -321,7 +321,7 @@ window.onbeforeunload = function (e) {
function save(e) {
if (e) e.preventDefault();
var save_btn = ebi("save"),
save_cls = save_btn.getAttribute('class') + '';
save_cls = save_btn.className + '';
if (save_cls.indexOf('disabled') >= 0)
return toast.inf(2, "no changes");
@@ -336,7 +336,7 @@ function save(e) {
fd.append("body", txt);
var url = (document.location + '').split('?')[0];
var xhr = new XMLHttpRequest();
var xhr = new XHR();
xhr.open('POST', url, true);
xhr.responseType = 'text';
xhr.onreadystatechange = save_cb;
@@ -356,7 +356,7 @@ function save(e) {
}
function save_cb() {
if (this.readyState != XMLHttpRequest.DONE)
if (this.readyState != XHR.DONE)
return;
if (this.status !== 200)
@@ -397,7 +397,7 @@ function save_cb() {
function run_savechk(lastmod, txt, btn, ntry) {
// download the saved doc from the server and compare
var url = (document.location + '').split('?')[0] + '?raw&_=' + Date.now();
var xhr = new XMLHttpRequest();
var xhr = new XHR();
xhr.open('GET', url, true);
xhr.responseType = 'text';
xhr.onreadystatechange = savechk_cb;
@@ -409,7 +409,7 @@ function run_savechk(lastmod, txt, btn, ntry) {
}
function savechk_cb() {
if (this.readyState != XMLHttpRequest.DONE)
if (this.readyState != XHR.DONE)
return;
if (this.status !== 200)
@@ -678,7 +678,7 @@ function reLastIndexOf(txt, ptn, end) {
// table formatter
function fmt_table(e) {
if (e) e.preventDefault();
//dom_tbox.setAttribute('class', '');
//dom_tbox.className = '';
var txt = dom_src.value,
ofs = dom_src.selectionStart,
@@ -829,7 +829,7 @@ function fmt_table(e) {
// show unicode
function mark_uni(e) {
if (e) e.preventDefault();
dom_tbox.setAttribute('class', '');
dom_tbox.className = '';
var txt = dom_src.value,
ptn = new RegExp('([^' + js_uni_whitelist + ']+)', 'g'),
@@ -989,14 +989,14 @@ var set_lno = (function () {
ebi('tools').onclick = function (e) {
if (e) e.preventDefault();
var is_open = dom_tbox.getAttribute('class') != 'open';
dom_tbox.setAttribute('class', is_open ? 'open' : '');
var is_open = dom_tbox.className != 'open';
dom_tbox.className = is_open ? 'open' : '';
};
ebi('help').onclick = function (e) {
if (e) e.preventDefault();
dom_tbox.setAttribute('class', '');
dom_tbox.className = '';
var dom = ebi('helpbox');
var dtxt = dom.getElementsByTagName('textarea');

View File

@@ -37,7 +37,7 @@ var lightswitch = (function () {
drk = l.light != 1,
f = function (e) {
if (e) drk = !drk;
document.documentElement.setAttribute("class", drk? "z":"y");
document.documentElement.className = drk? "z":"y";
l.light = drk? 0:1;
};
f();

View File

@@ -114,7 +114,7 @@ function save(mde) {
fd.append("body", txt);
var url = (document.location + '').split('?')[0];
var xhr = new XMLHttpRequest();
var xhr = new XHR();
xhr.open('POST', url, true);
xhr.responseType = 'text';
xhr.onreadystatechange = save_cb;
@@ -133,7 +133,7 @@ function save(mde) {
}
function save_cb() {
if (this.readyState != XMLHttpRequest.DONE)
if (this.readyState != XHR.DONE)
return;
if (this.status !== 200)
@@ -170,7 +170,7 @@ function save_cb() {
// download the saved doc from the server and compare
var url = (document.location + '').split('?')[0] + '?raw';
var xhr = new XMLHttpRequest();
var xhr = new XHR();
xhr.open('GET', url, true);
xhr.responseType = 'text';
xhr.onreadystatechange = save_chk;
@@ -182,7 +182,7 @@ function save_cb() {
}
function save_chk() {
if (this.readyState != XMLHttpRequest.DONE)
if (this.readyState != XHR.DONE)
return;
if (this.status !== 200)

View File

@@ -97,10 +97,15 @@
<a href="#" id="repl">π</a>
<script>
document.documentElement.setAttribute("class", localStorage.light == 1 ? "y" : "z");
document.documentElement.className = localStorage.light == 1 ? "y" : "z";
</script>
<script src="/.cpr/util.js?_={{ ts }}"></script>
<script>tt.init();</script>
<script>
tt.init();
{%- if this.uname == '*' %}
QS('input[name="cppwd"]').focus();
{%- endif %}
</script>
</body>
</html>

View File

@@ -11,6 +11,7 @@ html {
max-width: 34em;
max-width: min(34em, 90%);
max-width: min(34em, calc(100% - 7em));
color: #ddd;
background: #333;
border: 0 solid #777;
box-shadow: 0 .2em .5em #111;
@@ -158,6 +159,7 @@ html {
color: #f6a;
}
html.y #tt {
color: #333;
background: #fff;
border-color: #888 #000 #777 #000;
}

View File

@@ -135,7 +135,7 @@ function up2k_flagbus() {
}
function U2pvis(act, btns) {
function U2pvis(act, btns, uc) {
var r = this;
r.act = act;
r.ctr = { "ok": 0, "ng": 0, "bz": 0, "q": 0 };
@@ -425,7 +425,9 @@ function U2pvis(act, btns) {
html.push(r.genrow(a, true).replace(/><td>/, "><td>b "));
}
}
ebi('u2tab').tBodies[0].innerHTML = html.join('\n');
var el = ebi('u2tab');
el.tBodies[0].innerHTML = html.join('\n');
el.className = (uc.fsearch ? 'srch ' : 'up ') + r.act;
};
r.genrow = function (nfile, as_html) {
@@ -624,11 +626,11 @@ function up2k_init(subtle) {
function setmsg(msg, type) {
if (msg !== undefined) {
ebi('u2err').setAttribute('class', type);
ebi('u2err').className = type;
ebi('u2err').innerHTML = msg;
}
else {
ebi('u2err').setAttribute('class', '');
ebi('u2err').className = '';
ebi('u2err').innerHTML = '';
}
if (msg == suggest_up2k) {
@@ -665,8 +667,8 @@ function up2k_init(subtle) {
bcfg_bind(uc, 'ask_up', 'ask_up', true, null, false);
bcfg_bind(uc, 'flag_en', 'flag_en', false, apply_flag_cfg);
bcfg_bind(uc, 'fsearch', 'fsearch', false, set_fsearch, false);
bcfg_bind(uc, 'turbo', 'u2turbo', false, draw_turbo, false);
bcfg_bind(uc, 'datechk', 'u2tdate', true, null, false);
bcfg_bind(uc, 'turbo', 'u2turbo', turbolvl > 1, draw_turbo, false);
bcfg_bind(uc, 'datechk', 'u2tdate', turbolvl < 3, null, false);
var st = {
"files": [],
@@ -705,7 +707,7 @@ function up2k_init(subtle) {
});
}
var pvis = new U2pvis("bz", '#u2cards'),
var pvis = new U2pvis("bz", '#u2cards', uc),
donut = new Donut(uc, st);
var bobslice = null;
@@ -1809,11 +1811,8 @@ function up2k_init(subtle) {
tasker();
return;
}
toast.err(0, "server broke; hs-err {0} on file [{1}]:\n".format(
xhr.status, t.name) + (
(xhr.response && xhr.response.err) ||
(xhr.responseText && xhr.responseText) ||
"no further information"));
err = t.t_uploading ? "finalize upload" : t.srch ? "perform search" : "initiate upload";
xhrchk(xhr, "server rejected the request to " + err + ";\n\nfile: " + t.name + "\n\nerror ", "404, target folder not found");
}
}
xhr.onload = function (e) {
@@ -1872,8 +1871,7 @@ function up2k_init(subtle) {
console.log("ignoring dupe-segment error", t);
}
else {
toast.err(0, "server broke; cu-err {0} on file [{1}]:\n".format(
xhr.status, t.name) + (txt || "no further information"));
xhrchk(xhr, "server rejected upload (chunk {0} of {1});\n\nfile: {2}\n\nerror ".format(npart, Math.ceil(t.size / chunksize), t.name), "404, target folder not found (???)");
chill(t);
}
@@ -1902,7 +1900,7 @@ function up2k_init(subtle) {
return;
if (!toast.visible)
toast.warn(9.98, "failed to upload a chunk;\nprobably harmless, continuing\n\n" + t.name);
toast.warn(9.98, "failed to upload chunk {0} of {1};\nprobably harmless, continuing\n\nfile: {2}".format(npart, Math.ceil(t.size / chunksize), t.name));
console.log('chunkpit onerror,', ++tries, t);
orz2(xhr);
@@ -1938,19 +1936,17 @@ function up2k_init(subtle) {
if (btn.parentNode !== parent) {
parent.appendChild(btn);
ebi('u2conf').setAttribute('class', wide);
ebi('u2cards').setAttribute('class', wide);
ebi('u2etaw').setAttribute('class', wide);
ebi('u2conf').className = ebi('u2cards').className = ebi('u2etaw').className = wide;
}
wide = write && wem > 78 ? 'ww' : wide;
parent = ebi(wide == 'ww' && write ? 'u2c3w' : 'u2c3t');
var its = [ebi('u2etaw'), ebi('u2cards')];
if (its[0].parentNode !== parent) {
ebi('u2conf').setAttribute('class', wide);
ebi('u2conf').className = wide;
for (var a = 0; a < 2; a++) {
parent.appendChild(its[a]);
its[a].setAttribute('class', wide);
its[a].className = wide;
}
}
}
@@ -2024,9 +2020,12 @@ function up2k_init(subtle) {
html = ebi('u2foot').innerHTML,
ohtml = html;
if (uc.turbo && html.indexOf(msg) === -1)
if (turbolvl || !uc.turbo)
msg = null;
if (msg && html.indexOf(msg) === -1)
html = html.replace(omsg, '') + msg;
else if (!uc.turbo)
else if (!msg)
html = html.replace(msgu, '').replace(msgs, '');
if (html !== ohtml)
@@ -2070,6 +2069,8 @@ function up2k_init(subtle) {
}
catch (ex) { }
ebi('u2tab').className = (uc.fsearch ? 'srch ' : 'up ') + pvis.act;
draw_turbo();
onresize();
}

View File

@@ -14,7 +14,8 @@ var is_touch = 'ontouchstart' in window,
var ebi = document.getElementById.bind(document),
QS = document.querySelector.bind(document),
QSA = document.querySelectorAll.bind(document),
mknod = document.createElement.bind(document);
mknod = document.createElement.bind(document),
XHR = XMLHttpRequest;
function qsr(sel) {
@@ -89,6 +90,9 @@ function vis_exh(msg, url, lineNo, columnNo, error) {
if ((msg + '').indexOf('l2d.js') !== -1)
return; // `t` undefined in tapEvent -> hitTestSimpleCustom
if (!/\.js($|\?)/.exec('' + url))
return; // chrome debugger
var ekey = url + '\n' + lineNo + '\n' + msg;
if (ignexd[ekey] || crashed)
return;
@@ -327,7 +331,7 @@ function clgot(el, cls) {
if (el.classList)
return el.classList.contains(cls);
var lst = (el.getAttribute('class') + '').split(/ /g);
var lst = (el.className + '').split(/ /g);
return has(lst, cls);
}
@@ -1182,6 +1186,9 @@ var modal = (function () {
return ok();
}
if ((k == 'ArrowLeft' || k == 'ArrowRight') && eng && (ae == eok || ae == eng))
return (ae == eok ? eng : eok).focus() || ev(e);
if (k == 'Escape')
return ng();
}
@@ -1383,3 +1390,18 @@ var favico = (function () {
r.to = setTimeout(r.init, 100);
return r;
})();
function xhrchk(xhr, prefix, e404) {
if (xhr.status < 400 && xhr.status >= 200)
return true;
if (xhr.status == 403)
return toast.err(0, prefix + "403, access denied\n\ntry pressing F5, maybe you got logged out");
if (xhr.status == 404)
return toast.err(0, prefix + e404);
return toast.err(0, prefix + xhr.status + ": " + (
(xhr.response && xhr.response.err) || xhr.responseText));
}

4
docs/notes.bat Normal file
View File

@@ -0,0 +1,4 @@
rem appending a static ip to a dhcp nic on windows 10-1703 or later
netsh interface ipv4 show interface
netsh interface ipv4 set interface interface="Ethernet 2" dhcpstaticipcoexistence=enabled
netsh interface ipv4 add address "Ethernet 2" 10.1.2.4 255.255.255.0

View File

@@ -4,7 +4,7 @@ ENV ver_asmcrypto=5b994303a9d3e27e0915f72a10b6c2c51535a4dc \
ver_hashwasm=4.9.0 \
ver_marked=4.0.12 \
ver_mde=2.16.1 \
ver_codemirror=5.65.2 \
ver_codemirror=5.65.3 \
ver_fontawesome=5.13.0 \
ver_zopfli=1.0.3

View File

@@ -38,6 +38,9 @@ class Cfg(Namespace):
no_mv=False,
no_del=False,
no_zip=False,
no_thumb=False,
no_athumb=False,
no_vthumb=False,
no_voldump=True,
no_scandir=False,
no_sendfile=True,
@@ -53,6 +56,10 @@ class Cfg(Namespace):
textfiles="",
doctitle="",
html_head="",
theme=0,
themes=0,
turbo=0,
logout=573,
hist=None,
no_idx=None,
no_hash=None,

View File

@@ -17,7 +17,7 @@ from copyparty import util
class Cfg(Namespace):
def __init__(self, a=None, v=None, c=None):
ex = "nw e2d e2ds e2dsa e2t e2ts e2tsr no_logues no_readme no_acode force_js no_robots"
ex = "nw e2d e2ds e2dsa e2t e2ts e2tsr no_logues no_readme no_acode force_js no_robots no_thumb no_athumb no_vthumb"
ex = {k: False for k in ex.split()}
ex2 = {
"mtp": [],
@@ -36,6 +36,10 @@ class Cfg(Namespace):
"rsp_slp": 0,
"s_wr_slp": 0,
"s_wr_sz": 512 * 1024,
"theme": 0,
"themes": 0,
"turbo": 0,
"logout": 573,
}
ex.update(ex2)
super(Cfg, self).__init__(a=a or [], v=v or [], c=c, **ex)