Compare commits

..

42 Commits

Author SHA1 Message Date
ed
5f42237f2c v0.13.5 2021-08-16 08:40:26 +02:00
ed
68289cfa54 v0.13.4 2021-08-16 08:18:52 +02:00
ed
42ea30270f up2k-ui: post absolute URLs 2021-08-16 08:16:52 +02:00
ed
ebbbbf3d82 misc old-browser support 2021-08-16 00:22:30 +02:00
ed
27516e2d16 scroll navpane to open folder on load 2021-08-16 00:07:31 +02:00
ed
84bb6f915e fix unpost ui for nonroot volumes 2021-08-16 00:03:05 +02:00
ed
46752f758a fix bup into volumes with upload rules 2021-08-15 23:59:41 +02:00
ed
34c4c22e61 v0.13.3 2021-08-14 22:46:15 +02:00
ed
af2d0b8421 upgrade permsets in smoketest 2021-08-14 22:45:33 +02:00
ed
638b05a49a fix image-viewer touch handler 2021-08-14 22:40:54 +02:00
ed
7a13e8a7fc clear transform on 0deg rotate 2021-08-14 21:13:15 +02:00
ed
d9fa74711d cheaper shadows 2021-08-14 18:17:40 +02:00
ed
41867f578f image viewer: add rotation 2021-08-14 18:06:53 +02:00
ed
0bf41ed4ef exif orientation for thumbnails 2021-08-14 17:45:44 +02:00
ed
d080b4a731 v0.13.2 2021-08-12 22:42:36 +02:00
ed
ca4232ada9 move sortfiles from util to browser 2021-08-12 22:42:17 +02:00
ed
ad348f91c9 fix button placement in large modals 2021-08-12 22:31:28 +02:00
ed
990f915f42 ui tweaks 2021-08-12 22:31:07 +02:00
ed
53d720217b open videos in gallery 2021-08-12 22:30:52 +02:00
ed
7a06ff480d fix cut/paste on old chromes 2021-08-12 22:30:41 +02:00
ed
3ef551f788 selection-toggle in image viewer 2021-08-12 22:20:32 +02:00
ed
f0125cdc36 prevent massive stacks in chrome 2021-08-12 22:12:05 +02:00
ed
ed5f6736df add prisonparty systemd example 2021-08-10 23:29:14 +02:00
ed
15d8be0fae no more loops 2021-08-10 02:56:48 +02:00
ed
46f3e61360 no actually that is a terrible location 2021-08-09 23:53:09 +02:00
ed
87ad8c98d4 /var/empty is a good location 2021-08-09 23:37:01 +02:00
ed
9bbdc4100f fix permission flags in service scripts 2021-08-09 23:26:30 +02:00
ed
c80307e8ff v0.13.1 2021-08-09 22:28:54 +02:00
ed
c1d77e1041 add upload lifetimes 2021-08-09 22:17:41 +02:00
ed
d9e83650dc handle invalid XDG_CONFIG_HOME on linux 2021-08-09 22:13:16 +02:00
ed
f6d635acd9 sfx: return 1 on exception 2021-08-09 22:13:00 +02:00
ed
0dbd8a01ff mount PWD into chroot for config files 2021-08-09 22:12:39 +02:00
ed
8d755d41e0 per-volume rescan interval 2021-08-09 01:31:20 +02:00
ed
190473bd32 up2k-ui: fix hash-ahead button 2021-08-09 01:16:09 +02:00
ed
030d1ec254 no wait thats too much 2021-08-09 01:15:51 +02:00
ed
5a2b91a084 handle more exceptions + sanitize fs paths in msgs 2021-08-09 01:09:20 +02:00
ed
a50a05e4e7 git: set 0755 on binary 2021-08-09 00:44:19 +02:00
ed
6cb5a87c79 add chroot wrapper (tested on debian only) 2021-08-09 00:42:21 +02:00
ed
b9f89ca552 shared password for providers 2021-08-08 23:05:00 +02:00
ed
26c9fd5dea add converter to freg / yta-raw 2021-08-08 22:48:02 +02:00
ed
e81a9b6fe0 better error handling 2021-08-08 20:48:24 +02:00
ed
452450e451 improve youtube parser 2021-08-08 20:30:12 +02:00
31 changed files with 983 additions and 279 deletions

View File

@@ -248,13 +248,15 @@ the browser has the following hotkeys (assumes qwerty, ignores actual layout)
* when viewing images / playing videos:
* `J/L, Left/Right` prev/next file
* `Home/End` first/last file
* `S` toggle selection
* `R` rotate clockwise (shift=ccw)
* `Esc` close viewer
* videos:
* `U/O` skip 10sec back/forward
* `P/K/Space` play/pause
* `F` fullscreen
* `C` continue playing next video
* `R` loop
* `V` loop
* `M` mute
* when the navpane is open:
* `A/D` adjust tree width
@@ -487,6 +489,7 @@ you can set upload rules using volume flags, some examples:
* if someone uploads to `/foo/bar` the path would be rewritten to `/foo/bar/2021/08/06/23` for example
* but the actual value is not verified, just the structure, so the uploader can choose any values which conform to the format string
* just to avoid additional complexity in up2k which is enough of a mess already
* `:c,lifetime=300` delete uploaded files when they become 5 minutes old
you can also set transaction limits which apply per-IP and per-volume, but these assume `-j 1` (default) otherwise the limits will be off, for example `-j 4` would allow anywhere between 1x and 4x the limits you set depending on which processing node the client gets routed to
@@ -577,28 +580,32 @@ copyparty can invoke external programs to collect additional metadata for files
`ie` = internet-explorer, `ff` = firefox, `c` = chrome, `iOS` = iPhone/iPad, `Andr` = Android
| feature | ie6 | ie9 | ie10 | ie11 | ff 52 | c 49 | iOS | Andr |
| --------------- | --- | --- | ---- | ---- | ----- | ---- | --- | ---- |
| browse files | yep | yep | yep | yep | yep | yep | yep | yep |
| basic uploader | yep | yep | yep | yep | yep | yep | yep | yep |
| make directory | yep | yep | yep | yep | yep | yep | yep | yep |
| send message | yep | yep | yep | yep | yep | yep | yep | yep |
| set sort order | - | yep | yep | yep | yep | yep | yep | yep |
| zip selection | - | yep | yep | yep | yep | yep | yep | yep |
| navpane | - | - | `*1` | yep | yep | yep | yep | yep |
| up2k | - | - | yep | yep | yep | yep | yep | yep |
| markdown editor | - | - | yep | yep | yep | yep | yep | yep |
| markdown viewer | - | - | yep | yep | yep | yep | yep | yep |
| play mp3/m4a | - | yep | yep | yep | yep | yep | yep | yep |
| play ogg/opus | - | - | - | - | yep | yep | `*2` | yep |
| thumbnail view | - | - | - | - | yep | yep | yep | yep |
| image viewer | - | - | - | - | yep | yep | yep | yep |
| **= feature =** | ie6 | ie9 | ie10 | ie11 | ff 52 | c 49 | iOS | Andr |
| feature | ie6 | ie9 | ie10 | ie11 | ff 52 | c 49 | iOS | Andr |
| --------------- | --- | ---- | ---- | ---- | ----- | ---- | --- | ---- |
| browse files | yep | yep | yep | yep | yep | yep | yep | yep |
| thumbnail view | - | yep | yep | yep | yep | yep | yep | yep |
| basic uploader | yep | yep | yep | yep | yep | yep | yep | yep |
| up2k | - | - | `*1` | `*1` | yep | yep | yep | yep |
| make directory | yep | yep | yep | yep | yep | yep | yep | yep |
| send message | yep | yep | yep | yep | yep | yep | yep | yep |
| set sort order | - | yep | yep | yep | yep | yep | yep | yep |
| zip selection | - | yep | yep | yep | yep | yep | yep | yep |
| file rename | - | yep | yep | yep | yep | yep | yep | yep |
| file cut/paste | - | yep | yep | yep | yep | yep | yep | yep |
| navpane | - | `*2` | yep | yep | yep | yep | yep | yep |
| image viewer | - | yep | yep | yep | yep | yep | yep | yep |
| video player | - | yep | yep | yep | yep | yep | yep | yep |
| markdown editor | - | - | yep | yep | yep | yep | yep | yep |
| markdown viewer | - | - | yep | yep | yep | yep | yep | yep |
| play mp3/m4a | - | yep | yep | yep | yep | yep | yep | yep |
| play ogg/opus | - | - | - | - | yep | yep | `*3` | yep |
| **= feature =** | ie6 | ie9 | ie10 | ie11 | ff 52 | c 49 | iOS | Andr |
* internet explorer 6 to 8 behave the same
* firefox 52 and chrome 49 are the last winxp versions
* `*1` only public folders (login session is dropped) and no history / back-button
* `*2` using a wasm decoder which can sometimes get stuck and consumes a bit more power
* `*1` yes, but extremely slow (ie10: 1 MiB/s, ie11: 270 KiB/s)
* `*2` causes a full-page refresh on each navigation
* `*3` using a wasm decoder which can sometimes get stuck and consumes a bit more power
quick summary of more eccentric web-browsers trying to view a directory index:
@@ -610,6 +617,7 @@ quick summary of more eccentric web-browsers trying to view a directory index:
| **lynx** (2.8.9/macports) | can browse, login, upload/mkdir/msg |
| **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 (text is yellow on white), upload with `?b=u` |
| **SerenityOS** (7e98457) | hits a page fault, works with `?b=u`, file upload not-impl |

View File

@@ -61,3 +61,8 @@ cd /mnt/nas/music/.hist
~/src/copyparty/bin/dbtool.py -src up2k.*.v3 up2k.db -rm-mtp-flag -copy key
~/src/copyparty/bin/dbtool.py -src up2k.*.v3 up2k.db -rm-mtp-flag -copy .bpm -vac
```
# [`prisonparty.sh`](prisonparty.sh)
* run copyparty in a chroot, preventing any accidental file access
* creates bindmounts for /bin, /lib, and so on, see `sysdirs=`

39
bin/mtag/res/yt-ipr.conf Normal file
View File

@@ -0,0 +1,39 @@
# example config file to use copyparty as a youtube manifest collector,
# use with copyparty like: python copyparty.py -c yt-ipr.conf
#
# see docs/example.conf for a better explanation of the syntax, but
# newlines are block separators, so adding blank lines inside a volume definition is bad
# (use comments as separators instead)
# create user ed, password wark
u ed:wark
# create a volume at /ytm which stores files at ./srv/ytm
./srv/ytm
/ytm
# write-only, but read-write for user ed
w
rw ed
# rescan the volume on startup
c e2dsa
# collect tags from all new files since last scan
c e2ts
# optionally enable compression to make the files 50% smaller
c pk
# only allow uploads which are between 16k and 1m large
c sz=16k-1m
# allow up to 10 uploads over 5 minutes from each ip
c maxn=10,300
# move uploads into subfolders: YEAR-MONTH / DAY-HOUR / <upload>
c rotf=%Y-%m/%d-%H
# delete uploads when they are 24 hours old
c lifetime=86400
# add the parser and tell copyparty what tags it can expect from it
c mtp=yt-id,yt-title,yt-author,yt-channel,yt-views,yt-private,yt-manifest,yt-expires=bin/mtag/yt-ipr.py
# decide which tags we want to index and in what order
c mte=yt-id,yt-title,yt-author,yt-channel,yt-views,yt-private,yt-manifest,yt-expires
# create any other volumes you'd like down here, or merge this with an existing config file

View File

@@ -7,26 +7,41 @@
// ==/UserScript==
function main() {
var server = 'https://127.0.0.1:3923/ytm?pw=wark',
interval = 60; // sec
var sent = {};
function send(txt) {
if (sent[txt])
function send(txt, mf_url, desc) {
if (sent[mf_url])
return;
fetch('https://127.0.0.1:3923/playerdata?_=' + Date.now(), { method: "PUT", body: txt });
console.log('[yt-ipr] yeet %d bytes', txt.length);
sent[txt] = 1;
fetch(server + '&_=' + Date.now(), { method: "PUT", body: txt });
console.log('[yt-pdh] yeet %d bytes, %s', txt.length, desc);
sent[mf_url] = 1;
}
function collect() {
setTimeout(collect, 60 * 1000);
var pd = document.querySelector('ytd-watch-flexy');
if (pd)
send(JSON.stringify(pd.playerData));
try {
var pd = document.querySelector('ytd-watch-flexy');
if (!pd)
return console.log('[yt-pdh] no video found');
pd = pd.playerData;
var mu = pd.streamingData.dashManifestUrl || pd.streamingData.hlsManifestUrl;
if (!mu || !mu.length)
return console.log('[yt-pdh] no manifest found');
var desc = pd.videoDetails.videoId + ', ' + pd.videoDetails.title;
send(JSON.stringify(pd), mu, desc);
}
catch (ex) {
console.log("[yt-pdh]", ex);
}
}
setTimeout(collect, 5000);
setInterval(collect, interval * 1000);
}
var scr = document.createElement('script');
scr.textContent = '(' + main.toString() + ')();';
(document.head || document.getElementsByTagName('head')[0]).appendChild(scr);
console.log('[yt-ipr] a');
console.log('[yt-pdh] a');

View File

@@ -1,19 +1,26 @@
#!/usr/bin/env python
import re
import os
import sys
import gzip
import json
import base64
import string
import urllib.request
from datetime import datetime
"""
youtube initial player response
example usage:
-v srv/playerdata:playerdata:w
:c,e2tsr:c,e2dsa
:c,mtp=yt-id,yt-title,yt-author,yt-channel,yt-views,yt-private,yt-expires=bin/mtag/yt-ipr.py
:c,mte=yt-id,yt-title,yt-author,yt-channel,yt-views,yt-private,yt-expires
it's probably best to use this through a config file; see res/yt-ipr.conf
but if you want to use plain arguments instead then:
-v srv/ytm:ytm:w:rw,ed
:c,e2ts:c,e2dsa
:c,sz=16k-1m:c,maxn=10,300:c,rotf=%Y-%m/%d-%H
:c,mtp=yt-id,yt-title,yt-author,yt-channel,yt-views,yt-private,yt-manifest,yt-expires=bin/mtag/yt-ipr.py
:c,mte=yt-id,yt-title,yt-author,yt-channel,yt-views,yt-private,yt-manifest,yt-expires
see res/yt-ipr.user.js for the example userscript to go with this
"""
@@ -30,19 +37,36 @@ def main():
txt = "{" + txt.split("{", 1)[1]
try:
obj = json.loads(txt)
pd = json.loads(txt)
except json.decoder.JSONDecodeError as ex:
obj = json.loads(txt[: ex.pos])
pd = json.loads(txt[: ex.pos])
# print(json.dumps(obj, indent=2))
# print(json.dumps(pd, indent=2))
vd = obj["videoDetails"]
sd = obj["streamingData"]
if "videoDetails" in pd:
parse_youtube(pd)
else:
parse_freg(pd)
def get_expiration(url):
et = re.search(r"[?&]expire=([0-9]+)", url).group(1)
et = datetime.utcfromtimestamp(int(et))
return et.strftime("%Y-%m-%d, %H:%M")
def parse_youtube(pd):
vd = pd["videoDetails"]
sd = pd["streamingData"]
et = sd["adaptiveFormats"][0]["url"]
et = re.search(r"[?&]expire=([0-9]+)", et).group(1)
et = datetime.utcfromtimestamp(int(et))
et = et.strftime("%Y-%m-%d, %H:%M")
et = get_expiration(et)
mf = []
if "dashManifestUrl" in sd:
mf.append("dash")
if "hlsManifestUrl" in sd:
mf.append("hls")
r = {
"yt-id": vd["videoId"],
@@ -52,10 +76,123 @@ def main():
"yt-views": vd["viewCount"],
"yt-private": vd["isPrivate"],
# "yt-expires": sd["expiresInSeconds"],
"yt-manifest": ",".join(mf),
"yt-expires": et,
}
print(json.dumps(r))
freg_conv(pd)
def parse_freg(pd):
md = pd["metadata"]
r = {
"yt-id": md["id"],
"yt-title": md["title"],
"yt-author": md["channelName"],
"yt-channel": md["channelURL"].strip("/").split("/")[-1],
"yt-expires": get_expiration(list(pd["video"].values())[0]),
}
print(json.dumps(r))
def freg_conv(pd):
# based on getURLs.js v1.5 (2021-08-07)
# fmt: off
priority = {
"video": [
337, 315, 266, 138, # 2160p60
313, 336, # 2160p
308, # 1440p60
271, 264, # 1440p
335, 303, 299, # 1080p60
248, 169, 137, # 1080p
334, 302, 298, # 720p60
247, 136 # 720p
],
"audio": [
251, 141, 171, 140, 250, 249, 139
]
}
vid_id = pd["videoDetails"]["videoId"]
chan_id = pd["videoDetails"]["channelId"]
try:
thumb_url = pd["microformat"]["playerMicroformatRenderer"]["thumbnail"]["thumbnails"][0]["url"]
start_ts = pd["microformat"]["playerMicroformatRenderer"]["liveBroadcastDetails"]["startTimestamp"]
except:
thumb_url = f"https://img.youtube.com/vi/{vid_id}/maxresdefault.jpg"
start_ts = ""
# fmt: on
metadata = {
"title": pd["videoDetails"]["title"],
"id": vid_id,
"channelName": pd["videoDetails"]["author"],
"channelURL": "https://www.youtube.com/channel/" + chan_id,
"description": pd["videoDetails"]["shortDescription"],
"thumbnailUrl": thumb_url,
"startTimestamp": start_ts,
}
if [x for x in vid_id if x not in string.ascii_letters + string.digits + "_-"]:
print(f"malicious json", file=sys.stderr)
return
basepath = os.path.dirname(sys.argv[1])
thumb_fn = f"{basepath}/{vid_id}.jpg"
tmp_fn = f"{thumb_fn}.{os.getpid()}"
if not os.path.exists(thumb_fn) and (
thumb_url.startswith("https://img.youtube.com/vi/")
or thumb_url.startswith("https://i.ytimg.com/vi/")
):
try:
with urllib.request.urlopen(thumb_url) as fi:
with open(tmp_fn, "wb") as fo:
fo.write(fi.read())
os.rename(tmp_fn, thumb_fn)
except:
if os.path.exists(tmp_fn):
os.unlink(tmp_fn)
try:
with open(thumb_fn, "rb") as f:
thumb = base64.b64encode(f.read()).decode("ascii")
except:
thumb = "/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/yQALCAABAAEBAREA/8wABgAQEAX/2gAIAQEAAD8A0s8g/9k="
metadata["thumbnail"] = "data:image/jpeg;base64," + thumb
ret = {
"metadata": metadata,
"version": "1.5",
"createTime": datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"),
}
for stream, itags in priority.items():
for itag in itags:
url = None
for afmt in pd["streamingData"]["adaptiveFormats"]:
if itag == afmt["itag"]:
url = afmt["url"]
break
if url:
ret[stream] = {itag: url}
break
fn = f"{basepath}/{vid_id}.urls.json"
with open(fn, "w", encoding="utf-8", errors="replace") as f:
f.write(json.dumps(ret, indent=4))
if __name__ == "__main__":
main()
try:
main()
except:
# raise
pass

99
bin/prisonparty.sh Normal file
View File

@@ -0,0 +1,99 @@
#!/bin/bash
set -e
# runs copyparty (or any other program really) in a chroot
#
# assumption: these directories, and everything within, are owned by root
sysdirs=( /bin /lib /lib32 /lib64 /sbin /usr )
# error-handler
help() { cat <<'EOF'
usage:
./prisonparty.sh <ROOTDIR> <UID> <GID> [VOLDIR [VOLDIR...]] -- copyparty-sfx.py [...]"
example:
./prisonparty.sh /var/lib/copyparty-jail 1000 1000 /mnt/nas/music -- copyparty-sfx.py -v /mnt/nas/music::rwmd"
EOF
exit 1
}
# read arguments
trap help EXIT
jail="$(realpath "$1")"; shift
uid="$1"; shift
gid="$1"; shift
vols=()
while true; do
v="$1"; shift
[ "$v" = -- ] && break # end of volumes
[ "$#" -eq 0 ] && break # invalid usage
vols+=( "$(realpath "$v")" )
done
pybin="$1"; shift
pybin="$(realpath "$pybin")"
cpp="$1"; shift
cpp="$(realpath "$cpp")"
cppdir="$(dirname "$cpp")"
trap - EXIT
# debug/vis
echo
echo "chroot-dir = $jail"
echo "user:group = $uid:$gid"
echo " copyparty = $cpp"
echo
printf '\033[33m%s\033[0m\n' "copyparty can access these folders and all their subdirectories:"
for v in "${vols[@]}"; do
printf '\033[36m ├─\033[0m %s \033[36m ── added by (You)\033[0m\n' "$v"
done
printf '\033[36m ├─\033[0m %s \033[36m ── where the copyparty binary is\033[0m\n' "$cppdir"
printf '\033[36m ╰─\033[0m %s \033[36m ── the folder you are currently in\033[0m\n' "$PWD"
vols+=("$cppdir" "$PWD")
echo
# remove any trailing slashes
jail="${jail%/}"
cppdir="${cppdir%/}"
# bind-mount system directories and volumes
printf '%s\n' "${sysdirs[@]}" "${vols[@]}" | LC_ALL=C sort |
while IFS= read -r v; do
[ -e "$v" ] || {
# printf '\033[1;31mfolder does not exist:\033[0m %s\n' "/$v"
continue
}
i1=$(stat -c%D.%i "$v" 2>/dev/null || echo a)
i2=$(stat -c%D.%i "$jail$v" 2>/dev/null || echo b)
[ $i1 = $i2 ] && continue
mkdir -p "$jail$v"
mount --bind "$v" "$jail$v"
done
# create a tmp
mkdir -p "$jail/tmp"
chmod 777 "$jail/tmp"
# run copyparty
/sbin/chroot --userspec=$uid:$gid "$jail" "$pybin" "$cpp" "$@" && rv=0 || rv=$?
# cleanup if not in use
lsof "$jail" | grep -qF "$jail" &&
echo "chroot is in use, will not cleanup" ||
{
mount | grep -qF " on $jail" |
awk '{sub(/ type .*/,"");sub(/.* on /,"");print}' |
LC_ALL=C sort -r | tee /dev/stderr | tr '\n' '\0' | xargs -r0 umount
}
exit $rv

View File

@@ -8,11 +8,11 @@
#
# you may want to:
# change '/usr/bin/python' to another interpreter
# change '/mnt::a' to another location or permission-set
# change '/mnt::rw' to another location or permission-set
name="$SVCNAME"
command_background=true
pidfile="/var/run/$SVCNAME.pid"
command="/usr/bin/python /usr/local/bin/copyparty-sfx.py"
command_args="-q -v /mnt::a"
command_args="-q -v /mnt::rw"

View File

@@ -6,7 +6,7 @@
#
# you may want to:
# change '/usr/bin/python' to another interpreter
# change '/mnt::a' to another location or permission-set
# change '/mnt::rw' to another location or permission-set
#
# with `Type=notify`, copyparty will signal systemd when it is ready to
# accept connections; correctly delaying units depending on copyparty.
@@ -27,7 +27,7 @@ Description=copyparty file server
[Service]
Type=notify
SyslogIdentifier=copyparty
ExecStart=/usr/bin/python3 /usr/local/bin/copyparty-sfx.py -q -v /mnt::a
ExecStart=/usr/bin/python3 /usr/local/bin/copyparty-sfx.py -q -v /mnt::rw
ExecStartPre=/bin/bash -c 'mkdir -p /run/tmpfiles.d/ && echo "x /tmp/pe-copyparty*" > /run/tmpfiles.d/copyparty.conf'
[Install]

View File

@@ -0,0 +1,27 @@
# this will start `/usr/local/bin/copyparty-sfx.py`
# in a chroot, preventing accidental access elsewhere
# and share '/mnt' with anonymous read+write
#
# installation:
# 1) put copyparty-sfx.py and prisonparty.sh in /usr/local/bin
# 2) cp -pv prisonparty.service /etc/systemd/system && systemctl enable --now prisonparty
#
# you may want to:
# change '/mnt::rw' to another location or permission-set
# (remember to change the '/mnt' chroot arg too)
#
# enable line-buffering for realtime logging (slight performance cost):
# inside the [Service] block, add the following line:
# Environment=PYTHONUNBUFFERED=x
[Unit]
Description=copyparty file server
[Service]
SyslogIdentifier=prisonparty
WorkingDirectory=/usr/local/bin
ExecStart=/bin/bash /usr/local/bin/prisonparty.sh /var/lib/copyparty-jail 1000 1000 /mnt -- \
/usr/bin/python3 /usr/local/bin/copyparty-sfx.py -q -v /mnt::rw
[Install]
WantedBy=multi-user.target

View File

@@ -25,6 +25,28 @@ ANYWIN = WINDOWS or sys.platform in ["msys"]
MACOS = platform.system() == "Darwin"
def get_unix_home():
try:
v = os.environ["XDG_CONFIG_HOME"]
if not v:
raise Exception()
ret = os.path.normpath(v)
os.listdir(ret)
return ret
except:
pass
try:
v = os.path.expanduser("~/.config")
if v.startswith("~"):
raise Exception()
ret = os.path.normpath(v)
os.listdir(ret)
return ret
except:
return "/tmp"
class EnvParams(object):
def __init__(self):
self.t0 = time.time()
@@ -37,10 +59,7 @@ class EnvParams(object):
elif sys.platform == "darwin":
self.cfg = os.path.expanduser("~/Library/Preferences/copyparty")
else:
self.cfg = os.path.normpath(
os.getenv("XDG_CONFIG_HOME", os.path.expanduser("~/.config"))
+ "/copyparty"
)
self.cfg = get_unix_home() + "/copyparty"
self.cfg = self.cfg.replace("\\", "/")
try:

View File

@@ -210,9 +210,9 @@ def run_argparse(argv, formatter):
dedent(
"""
-a takes username:password,
-v takes src:dst:perm1:perm2:permN:cflag1:cflag2:cflagN:...
-v takes src:dst:perm1:perm2:permN:volflag1:volflag2:volflagN:...
where "perm" is "accesslevels,username1,username2,..."
and "cflag" is config flags to set on this volume
and "volflag" is config flags to set on this volume
list of accesslevels:
"r" (read): list folder contents, download files
@@ -220,7 +220,7 @@ def run_argparse(argv, formatter):
"m" (move): move files and folders; need "w" at destination
"d" (delete): permanently delete files and folders
too many cflags to list here, see the other sections
too many volflags to list here, see the other sections
example:\033[35m
-a ed:hunter2 -v .::r:rw,ed -v ../inc:dump:w:rw,ed:c,nodupe \033[36m
@@ -241,11 +241,11 @@ def run_argparse(argv, formatter):
),
],
[
"cflags",
"list of cflags",
"flags",
"list of volflags",
dedent(
"""
cflags are appended to volume definitions, for example,
volflags are appended to volume definitions, for example,
to create a write-only volume with the \033[33mnodupe\033[0m and \033[32mnosub\033[0m flags:
\033[35m-v /mnt/inc:/inc:w\033[33m:c,nodupe\033[32m:c,nosub
@@ -264,13 +264,15 @@ def run_argparse(argv, formatter):
(moves all uploads into the specified folder structure)
\033[36mrotn=100,3\033[35m 3 levels of subfolders with 100 entries in each
\033[36mrotf=%Y-%m/%d-%H\033[35m date-formatted organizing
\033[36mlifetime=3600\033[35m uploads are deleted after 1 hour
\033[0mdatabase, general:
\033[36me2d\033[35m sets -e2d (all -e2* args can be set using ce2* cflags)
\033[36me2d\033[35m sets -e2d (all -e2* args can be set using ce2* volflags)
\033[36md2t\033[35m disables metadata collection, overrides -e2t*
\033[36md2d\033[35m disables all database stuff, overrides -e2*
\033[36mdhash\033[35m disables file hashing on initial scans, also ehash
\033[36mhist=/tmp/cdb\033[35m puts thumbnails and indexes at that location
\033[36mscan=60\033[35m scan for new files every 60sec, same as --re-maxage
\033[0mdatabase, audio tags:
"mte", "mth", "mtp", "mtm" all work the same as -mte, -mth, ...
@@ -353,6 +355,7 @@ def run_argparse(argv, formatter):
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("--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]")
@@ -375,6 +378,7 @@ def run_argparse(argv, formatter):
ap2.add_argument("--no-thumb", action="store_true", help="disable all thumbnails")
ap2.add_argument("--no-vthumb", action="store_true", help="disable video thumbnails")
ap2.add_argument("--th-size", metavar="WxH", default="320x256", help="thumbnail res")
ap2.add_argument("--th-mt", metavar="CORES", type=int, default=0, help="max num cpu cores to use, 0=all")
ap2.add_argument("--th-no-crop", action="store_true", help="dynamic height; show full image")
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")
@@ -391,7 +395,7 @@ def run_argparse(argv, formatter):
ap2.add_argument("--hist", metavar="PATH", type=u, help="where to store volume data (db, thumbs)")
ap2.add_argument("--no-hash", action="store_true", help="disable hashing during e2ds folder scans")
ap2.add_argument("--re-int", metavar="SEC", type=int, default=30, help="disk rescan check interval")
ap2.add_argument("--re-maxage", metavar="SEC", type=int, default=0, help="disk rescan volume interval (0=off)")
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 = ap.add_argument_group('metadata db options')

View File

@@ -1,8 +1,8 @@
# coding: utf-8
VERSION = (0, 13, 0)
VERSION = (0, 13, 5)
CODENAME = "future-proof"
BUILD_DT = (2021, 8, 8)
BUILD_DT = (2021, 8, 16)
S_VERSION = ".".join(map(str, VERSION))
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)

View File

@@ -25,6 +25,9 @@ from .util import (
from .bos import bos
LEELOO_DALLAS = "leeloo_dallas"
class AXS(object):
def __init__(self, uread=None, uwrite=None, umove=None, udel=None):
self.uread = {} if uread is None else {k: 1 for k in uread}
@@ -327,7 +330,7 @@ class VFS(object):
[will_move, c.umove, "move"],
[will_del, c.udel, "delete"],
]:
if req and (uname not in d and "*" not in d):
if req and (uname not in d and "*" not in d) and uname != LEELOO_DALLAS:
m = "you don't have {}-access for this location"
raise Pebkac(403, m.format(msg))
@@ -554,6 +557,9 @@ class AuthSrv(object):
def _read_vol_str(self, lvl, uname, axs, flags):
# type: (str, str, AXS, any) -> None
if lvl.strip("crwmd"):
raise Exception("invalid volume flag: {},{}".format(lvl, uname))
if lvl == "c":
cval = True
if "=" in uname:
@@ -709,6 +715,9 @@ class AuthSrv(object):
)
raise Exception("invalid config")
if LEELOO_DALLAS in all_users:
raise Exception("sorry, reserved username: " + LEELOO_DALLAS)
promote = []
demote = []
for vol in vfs.all_vols.values():
@@ -807,6 +816,11 @@ class AuthSrv(object):
if "pk" in vol.flags and "gz" not in vol.flags and "xz" not in vol.flags:
vol.flags["gz"] = False # def.pk
if "scan" in vol.flags:
vol.flags["scan"] = int(vol.flags["scan"])
elif self.args.re_maxage:
vol.flags["scan"] = self.args.re_maxage
all_mte = {}
errors = False
for vol in vfs.all_vols.values():

View File

@@ -112,7 +112,7 @@ class HttpCli(object):
self.http_ver = "HTTP/1.1"
# self.log("pebkac at httpcli.run #1: " + repr(ex))
self.keepalive = False
self.loud_reply(unicode(ex), status=ex.code)
self.loud_reply(unicode(ex), status=ex.code, volsan=True)
return self.keepalive
# time.sleep(0.4)
@@ -224,19 +224,24 @@ class HttpCli(object):
else:
raise Pebkac(400, 'invalid HTTP mode "{0}"'.format(self.mode))
except Pebkac as ex:
except Exception as ex:
pex = ex
if not hasattr(ex, "code"):
pex = Pebkac(500)
try:
# self.log("pebkac at httpcli.run #2: " + repr(ex))
post = self.mode in ["POST", "PUT"] or "content-length" in self.headers
if not self._check_nonfatal(ex, post):
if not self._check_nonfatal(pex, post):
self.keepalive = False
self.log("{}\033[0m, {}".format(str(ex), self.vpath), 3)
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)
if self.hint:
msg += "hint: {}\r\n".format(self.hint)
self.reply(msg.encode("utf-8", "replace"), status=ex.code)
self.reply(msg.encode("utf-8", "replace"), status=pex.code, volsan=True)
return self.keepalive
except Pebkac:
return False
@@ -269,8 +274,12 @@ class HttpCli(object):
except:
raise Pebkac(400, "client d/c while replying headers")
def reply(self, body, status=200, mime=None, headers=None):
def reply(self, body, status=200, mime=None, headers=None, volsan=False):
# TODO something to reply with user-supplied values safely
if volsan:
body = vol_san(self.asrv.vfs.all_vols.values(), body)
self.send_headers(len(body), status, mime, headers)
try:
@@ -947,10 +956,14 @@ class HttpCli(object):
vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True)
self._assert_safe_rem(rem)
upload_vpath = self.vpath
lim = vfs.get_dbv(rem)[0].lim
fdir_base = os.path.join(vfs.realpath, rem)
if lim:
fdir_base, rem = lim.all(self.ip, rem, -1, fdir_base)
upload_vpath = "{}/{}".format(vfs.vpath, rem).strip("/")
if not nullwrite:
bos.makedirs(fdir_base)
files = []
errmsg = ""
@@ -977,8 +990,6 @@ class HttpCli(object):
if lim:
lim.chk_bup(self.ip)
lim.chk_nup(self.ip)
if not nullwrite:
bos.makedirs(fdir)
try:
with ren_open(fname, "wb", 512 * 1024, **open_args) as f:
@@ -1029,7 +1040,9 @@ class HttpCli(object):
raise
except Pebkac as ex:
errmsg = unicode(ex)
errmsg = vol_san(
self.asrv.vfs.all_vols.values(), unicode(ex).encode("utf-8")
).decode("utf-8")
td = max(0.1, time.time() - t0)
sz_total = sum(x[0] for x in files)
@@ -1049,7 +1062,7 @@ class HttpCli(object):
errmsg = "ERROR: " + errmsg
for sz, sha512, ofn, lfn in files:
vpath = (self.vpath + "/" if self.vpath else "") + lfn
vpath = "{}/{}".format(upload_vpath, lfn).strip("/")
msg += 'sha512: {} // {} bytes // <a href="/{}">{}</a>\n'.format(
sha512[:56], sz, quotep(vpath), html_escape(ofn, crlf=True)
)
@@ -1686,7 +1699,7 @@ class HttpCli(object):
q = "select sz, rd, fn, at from up where ip=? and at>?"
for sz, rd, fn, at in cur.execute(q, (self.ip, lim)):
vp = "/" + "/".join([rd, fn]).strip("/")
vp = "/" + "/".join(x for x in [vol.vpath, rd, fn] if x)
if filt and filt not in vp:
continue

View File

@@ -21,7 +21,7 @@ HAVE_AVIF = False
HAVE_WEBP = False
try:
from PIL import Image, ImageOps
from PIL import Image, ImageOps, ExifTags
HAVE_PIL = True
try:
@@ -105,7 +105,10 @@ class ThumbSrv(object):
self.mutex = threading.Lock()
self.busy = {}
self.stopping = False
self.nthr = os.cpu_count() if hasattr(os, "cpu_count") else 4
self.nthr = self.args.th_mt
if not self.nthr:
self.nthr = os.cpu_count() if hasattr(os, "cpu_count") else 4
self.q = Queue(self.nthr * 4)
for n in range(self.nthr):
t = threading.Thread(
@@ -221,21 +224,38 @@ class ThumbSrv(object):
with self.mutex:
self.nthr -= 1
def fancy_pillow(self, im):
# exif_transpose is expensive (loads full image + unconditional copy)
r = max(*self.res) * 2
im.thumbnail((r, r), resample=Image.LANCZOS)
try:
k = next(k for k, v in ExifTags.TAGS.items() if v == "Orientation")
exif = im.getexif()
rot = int(exif[k])
del exif[k]
except:
rot = 1
rots = {8: Image.ROTATE_90, 3: Image.ROTATE_180, 6: Image.ROTATE_270}
if rot in rots:
im = im.transpose(rots[rot])
if self.args.th_no_crop:
im.thumbnail(self.res, resample=Image.LANCZOS)
else:
iw, ih = im.size
dw, dh = self.res
res = (min(iw, dw), min(ih, dh))
im = ImageOps.fit(im, res, method=Image.LANCZOS)
return im
def conv_pil(self, abspath, tpath):
with Image.open(fsenc(abspath)) as im:
crop = not self.args.th_no_crop
res2 = self.res
if crop:
res2 = (res2[0] * 2, res2[1] * 2)
try:
im.thumbnail(res2, resample=Image.LANCZOS)
if crop:
iw, ih = im.size
dw, dh = self.res
res = (min(iw, dw), min(ih, dh))
im = ImageOps.fit(im, res, method=Image.LANCZOS)
except:
im = self.fancy_pillow(im)
except Exception as ex:
self.log("fancy_pillow {}".format(ex), "1;30")
im.thumbnail(self.res)
fmts = ["RGB", "L"]
@@ -289,6 +309,7 @@ class ThumbSrv(object):
b"-map", b"0:v:0",
b"-vf", scale,
b"-frames:v", b"1",
b"-metadata:s:v:0", b"rotate=0",
]
# fmt: on
@@ -306,6 +327,7 @@ class ThumbSrv(object):
]
cmd += [fsenc(tpath)]
# self.log((b" ".join(cmd)).decode("utf-8"))
ret, sout, serr = runcmd(cmd)
if ret != 0:

View File

@@ -36,7 +36,7 @@ from .util import (
min_ex,
)
from .bos import bos
from .authsrv import AuthSrv
from .authsrv import AuthSrv, LEELOO_DALLAS
from .mtag import MTag, MParser
try:
@@ -176,36 +176,71 @@ class Up2k(object):
return None
def _sched_rescan(self):
maxage = self.args.re_maxage
volage = {}
while True:
time.sleep(self.args.re_int)
now = time.time()
vpaths = list(sorted(self.asrv.vfs.all_vols.keys()))
with self.mutex:
if maxage:
for vp in vpaths:
if vp not in volage:
volage[vp] = now
for vp, vol in sorted(self.asrv.vfs.all_vols.items()):
maxage = vol.flags.get("scan")
if not maxage:
continue
if now - volage[vp] >= maxage:
self.need_rescan[vp] = 1
if vp not in volage:
volage[vp] = now
if not self.need_rescan:
continue
if now - volage[vp] >= maxage:
self.need_rescan[vp] = 1
vols = list(sorted(self.need_rescan.keys()))
self.need_rescan = {}
err = self.rescan(self.asrv.vfs.all_vols, vols)
if err:
for v in vols:
self.need_rescan[v] = True
if vols:
err = self.rescan(self.asrv.vfs.all_vols, vols)
if err:
for v in vols:
self.need_rescan[v] = True
continue
for v in vols:
volage[v] = now
if self.args.no_lifetime:
continue
for v in vols:
volage[v] = now
for vp, vol in sorted(self.asrv.vfs.all_vols.items()):
lifetime = vol.flags.get("lifetime")
if not lifetime:
continue
cur = self.cur.get(vol.realpath)
if not cur:
continue
nrm = 0
deadline = time.time() - int(lifetime)
q = "select rd, fn from up where at > 0 and at < ? limit 100"
while True:
with self.mutex:
hits = cur.execute(q, (deadline,)).fetchall()
if not hits:
break
for rd, fn in hits:
if rd.startswith("//") or fn.startswith("//"):
rd, fn = s3dec(rd, fn)
fvp = "{}/{}".format(rd, fn).strip("/")
if vp:
fvp = "{}/{}".format(vp, fvp)
self._handle_rm(LEELOO_DALLAS, None, fvp)
nrm += 1
if nrm:
self.log("{} files graduated in {}".format(nrm, vp))
def _vis_job_progress(self, job):
perc = 100 - (len(job["need"]) * 100.0 / len(job["hash"]))
@@ -1188,7 +1223,8 @@ class Up2k(object):
self._new_upload(job)
purl = "/{}/".format("{}/{}".format(job["vtop"], job["prel"]).strip("/"))
purl = "{}/{}".format(job["vtop"], job["prel"]).strip("/")
purl = "/{}/".format(purl) if purl else "/"
return {
"name": job["name"],

View File

@@ -344,6 +344,13 @@ def log_thrs(log, ival, name):
log(name, "\033[0m \033[33m".join(tv), 3)
def vol_san(vols, txt):
for vol in vols:
txt = txt.replace(vol.realpath.encode("utf-8"), vol.vpath.encode("utf-8"))
return txt
def min_ex():
et, ev, tb = sys.exc_info()
tb = traceback.extract_tb(tb)

View File

@@ -22,7 +22,7 @@ window.baguetteBox = (function () {
afterHide: null,
onChange: null,
},
overlay, slider, btnPrev, btnNext, btnHelp, btnVmode, btnClose,
overlay, slider, btnPrev, btnNext, btnHelp, btnRotL, btnRotR, btnSel, btnVmode, btnClose,
currentGallery = [],
currentIndex = 0,
isOverlayVisible = false,
@@ -49,7 +49,7 @@ window.baguetteBox = (function () {
};
var touchstartHandler = function (e) {
touch.count++;
touch.count = e.touches.length;
if (touch.count > 1)
touch.multitouch = true;
@@ -72,8 +72,11 @@ window.baguetteBox = (function () {
hideOverlay();
}
};
var touchendHandler = function () {
var touchendHandler = function (e) {
touch.count--;
if (e && e.touches)
touch.count = e.touches.length;
if (touch.count <= 0)
touch.multitouch = false;
@@ -175,6 +178,9 @@ window.baguetteBox = (function () {
'<button id="bbox-next" class="bbox-btn" type="button" aria-label="Next">&gt;</button>' +
'<div id="bbox-btns">' +
'<button id="bbox-help" type="button">?</button>' +
'<button id="bbox-rotl" type="button">↶</button>' +
'<button id="bbox-rotr" type="button">↷</button>' +
'<button id="bbox-tsel" type="button">sel</button>' +
'<button id="bbox-vmode" type="button" tt="a"></button>' +
'<button id="bbox-close" type="button" aria-label="Close">X</button>' +
'</div></div>'
@@ -187,6 +193,9 @@ window.baguetteBox = (function () {
btnPrev = ebi('bbox-prev');
btnNext = ebi('bbox-next');
btnHelp = ebi('bbox-help');
btnRotL = ebi('bbox-rotl');
btnRotR = ebi('bbox-rotr');
btnSel = ebi('bbox-tsel');
btnVmode = ebi('bbox-vmode');
btnClose = ebi('bbox-close');
bindEvents();
@@ -203,11 +212,13 @@ window.baguetteBox = (function () {
['right, L', 'next file'],
['home', 'first file'],
['end', 'last file'],
['R', 'rotate (shift=ccw)'],
['S', 'toggle file selection'],
['space, P, K', 'video: play / pause'],
['U', 'video: seek 10sec back'],
['P', 'video: seek 10sec ahead'],
['M', 'video: toggle mute'],
['R', 'video: toggle loop'],
['V', 'video: toggle loop'],
['C', 'video: toggle auto-next'],
['F', 'video: toggle fullscreen'],
],
@@ -249,7 +260,7 @@ window.baguetteBox = (function () {
v.muted = vmute = !vmute;
mp_ctl();
}
else if (k == "KeyR" && v) {
else if (k == "KeyV" && v) {
vloop = !vloop;
vnext = vnext && !vloop;
setVmode();
@@ -267,6 +278,10 @@ window.baguetteBox = (function () {
v.requestFullscreen();
}
catch (ex) { }
else if (k == "KeyS")
tglsel();
else if (k == "KeyR")
rotn(e.shiftKey ? -1 : 1);
}
function setVmode() {
@@ -279,7 +294,7 @@ window.baguetteBox = (function () {
if (vloop) {
lbl = 'Loop';
msg += 'repeat it';
tts = '$NHotkey: R';
tts = '$NHotkey: V';
}
else if (vnext) {
lbl = 'Cont';
@@ -314,6 +329,40 @@ window.baguetteBox = (function () {
tt.show.bind(this)();
}
function tglsel() {
var thumb = currentGallery[currentIndex].imageElement,
name = vsplit(thumb.href)[1],
files = msel.getall();
for (var a = 0; a < files.length; a++)
if (vsplit(files[a].vp)[1] == name)
clmod(ebi(files[a].id).closest('tr'), 'sel', 't');
msel.selui();
selbg();
}
function selbg() {
var img = vidimg(),
thumb = currentGallery[currentIndex].imageElement,
name = vsplit(thumb.href)[1],
files = msel.getsel(),
sel = false;
for (var a = 0; a < files.length; a++)
if (vsplit(files[a].vp)[1] == name)
sel = true;
ebi('bbox-overlay').style.background = sel ?
'rgba(153,34,85,0.7)' : '';
img.style.borderRadius = sel ? '1em' : '';
btnSel.style.color = sel ? '#fff' : '';
btnSel.style.background = sel ? '#d48' : '';
btnSel.style.textShadow = sel ? '1px 1px 0 #b38' : '';
btnSel.style.boxShadow = sel ? '.15em .15em 0 #502' : '';
}
function keyUpHandler(e) {
if (e.ctrlKey || e.altKey || e.metaKey || e.isComposing)
return;
@@ -348,6 +397,9 @@ window.baguetteBox = (function () {
bind(btnClose, 'click', hideOverlay);
bind(btnVmode, 'click', tglVmode);
bind(btnHelp, 'click', halp);
bind(btnRotL, 'click', rotl);
bind(btnRotR, 'click', rotr);
bind(btnSel, 'click', tglsel);
bind(slider, 'contextmenu', contextmenuHandler);
bind(overlay, 'touchstart', touchstartHandler, nonPassiveEvent);
bind(overlay, 'touchmove', touchmoveHandler, passiveEvent);
@@ -362,11 +414,15 @@ window.baguetteBox = (function () {
unbind(btnClose, 'click', hideOverlay);
unbind(btnVmode, 'click', tglVmode);
unbind(btnHelp, 'click', halp);
unbind(btnRotL, 'click', rotl);
unbind(btnRotR, 'click', rotr);
unbind(btnSel, 'click', tglsel);
unbind(slider, 'contextmenu', contextmenuHandler);
unbind(overlay, 'touchstart', touchstartHandler, nonPassiveEvent);
unbind(overlay, 'touchmove', touchmoveHandler, passiveEvent);
unbind(overlay, 'touchend', touchendHandler);
unbind(document, 'focus', trapFocusInsideOverlay, true);
timer.rm(rotn);
}
function prepareOverlay(gallery, userOptions) {
@@ -617,10 +673,91 @@ window.baguetteBox = (function () {
return true;
}
var prev_cw = 0, prev_ch = 0, unrot_timer = null;
function rotn(n) {
var el = vidimg(),
orot = parseInt(el.getAttribute('rot') || 0),
frot = orot + (n || 0) * 90;
if (!frot && !orot)
return; // reflow noop
var co = ebi('bbox-overlay'),
cw = co.clientWidth,
ch = co.clientHeight;
if (!n && prev_cw === cw && prev_ch === ch)
return; // reflow noop
prev_cw = cw;
prev_ch = ch;
var rot = frot,
iw = el.naturalWidth || el.videoWidth,
ih = el.naturalHeight || el.videoHeight,
magic = 4, // idk, works in enough browsers
dl = el.closest('div').querySelector('figcaption a'),
vw = cw,
vh = ch - dl.offsetHeight + magic,
pmag = Math.min(1, Math.min(vw / ih, vh / iw)),
wmag = Math.min(1, Math.min(vw / iw, vh / ih));
while (rot < 0) rot += 360;
while (rot >= 360) rot -= 360;
var q = rot == 90 || rot == 270 ? 1 : 0,
mag = q ? pmag : wmag;
el.style.cssText = 'max-width:none; max-height:none; position:absolute; display:block; margin:0';
if (!orot) {
el.style.width = iw * wmag + 'px';
el.style.height = ih * wmag + 'px';
el.style.left = (vw - iw * wmag) / 2 + 'px';
el.style.top = (vh - ih * wmag) / 2 - magic + 'px';
q = el.offsetHeight;
}
el.style.width = iw * mag + 'px';
el.style.height = ih * mag + 'px';
el.style.left = (vw - iw * mag) / 2 + 'px';
el.style.top = (vh - ih * mag) / 2 - magic + 'px';
el.style.transform = 'rotate(' + frot + 'deg)';
el.setAttribute('rot', frot);
timer.add(rotn);
if (!rot) {
clearTimeout(unrot_timer);
unrot_timer = setTimeout(unrot, 300);
}
}
function rotl() {
rotn(-1);
}
function rotr() {
rotn(1);
}
function unrot() {
var el = vidimg(),
orot = el.getAttribute('rot'),
rot = parseInt(orot || 0);
while (rot < 0) rot += 360;
while (rot >= 360) rot -= 360;
if (rot || orot === null)
return;
clmod(el, 'nt', 1);
el.removeAttribute('rot');
el.removeAttribute("style");
rot = el.offsetHeight;
clmod(el, 'nt');
timer.rm(rotn);
}
function vid() {
return imagesElements[currentIndex].querySelector('video');
}
function vidimg() {
return imagesElements[currentIndex].querySelector('img, video');
}
function playvid(play) {
if (vid())
vid()[play ? 'play' : 'pause']();
@@ -662,15 +799,21 @@ window.baguetteBox = (function () {
}
function updateOffset() {
var offset = -currentIndex * 100 + '%';
var offset = -currentIndex * 100 + '%',
xform = slider.style.perspective !== undefined;
if (options.animation === 'fadeIn') {
slider.style.opacity = 0;
setTimeout(function () {
slider.style.transform = 'translate3d(' + offset + ',0,0)';
xform ?
slider.style.transform = 'translate3d(' + offset + ',0,0)' :
slider.style.left = offset;
slider.style.opacity = 1;
}, 400);
} else {
slider.style.transform = 'translate3d(' + offset + ',0,0)';
xform ?
slider.style.transform = 'translate3d(' + offset + ',0,0)' :
slider.style.left = offset;
}
playvid(false);
var v = vid();
@@ -679,8 +822,21 @@ window.baguetteBox = (function () {
v.muted = vmute;
v.loop = vloop;
}
selbg();
mp_ctl();
setVmode();
var el = vidimg();
if (el.getAttribute('rot'))
timer.add(rotn);
else
timer.rm(rotn);
var prev = QS('.full-image.vis');
if (prev)
clmod(prev, 'vis');
clmod(el.closest('div'), 'vis', 1);
}
function preloadNext(index) {

View File

@@ -417,17 +417,10 @@ html.light #ggrid a.sel {
html.light #wfm a:not(.en) {
color: #c4a;
}
#files tbody tr.c1 td {
animation: fcut1 .5s ease-out;
#files tbody tr.fcut td {
animation: fcut .5s ease-out;
}
#files tbody tr.c2 td {
animation: fcut2 .5s ease-out;
}
@keyframes fcut1 {
0% {opacity:0}
100% {opacity:1}
}
@keyframes fcut2 {
@keyframes fcut {
0% {opacity:0}
100% {opacity:1}
}
@@ -709,6 +702,10 @@ input.eq_gain {
position: sticky;
z-index: 1;
top: 0;
height: 2.2em;
line-height: 2.2em;
border-bottom: 1px solid #555;
overflow: hidden;
}
#thx_ff {
padding: 5em 0;
@@ -896,6 +893,7 @@ html.light #ghead {
}
#ggrid a {
display: inline-block;
width: 10em;
width: var(--grid-sz);
vertical-align: top;
overflow-wrap: break-word;
@@ -909,7 +907,9 @@ html.light #ghead {
}
#ggrid a img {
border-radius: .2em;
max-width: 10em;
max-width: var(--grid-sz);
max-height: 8em;
max-height: calc(var(--grid-sz)/1.25);
margin: 0 auto;
display: block;
@@ -1251,6 +1251,7 @@ html.light #files tr.sel a:hover {
}
html.light #treeh {
background: #eee;
border-color: #ddd;
}
html.light #tree {
scrollbar-color: #a70 #ddd;
@@ -1294,7 +1295,7 @@ html.light #tree::-webkit-scrollbar {
opacity: 0;
position: fixed;
overflow: hidden;
touch-action: none;
touch-action: pinch-zoom;
top: 0;
left: 0;
width: 100%;
@@ -1328,6 +1329,14 @@ html.light #tree::-webkit-scrollbar {
max-height: calc(100% - 1.4em);
margin-bottom: 1.4em;
vertical-align: middle;
transition: transform .23s, left .23s, top .23s, width .23s, height .23s;
}
.full-image img.nt,
.full-image video.nt {
transition: none;
}
.full-image.vis img,
.full-image.vis video {
box-shadow: 0 0 8px rgba(0, 0, 0, 0.6);
}
.full-image video {
@@ -1341,6 +1350,7 @@ html.light #tree::-webkit-scrollbar {
text-align: center;
white-space: normal;
color: #ccc;
z-index: 1;
}
#bbox-overlay figcaption a {
background: rgba(0, 0, 0, 0.6);
@@ -1406,6 +1416,7 @@ html.light #bbox-overlay figcaption a {
font-size: 1.4em;
line-height: 1.4em;
vertical-align: top;
font-variant: small-caps;
}
#bbox-overlay button:focus,
#bbox-overlay button:hover {

View File

@@ -160,6 +160,7 @@ ebi('tree').innerHTML = (
' <a href="#" class="btn" step="2" id="twobytwo" tt="Hotkey: A">+</a>\n' +
' <a href="#" class="btn" step="-2" id="twig" tt="Hotkey: D">&ndash;</a>\n' +
' <a href="#" class="tgl btn" id="dyntree" tt="autogrow as tree expands">a</a>\n' +
' <a href="#" class="btn" id="visdir" tt="scroll to selected folder">v</a>\n' +
'</div>\n' +
'<ul id="treeul"></ul>\n' +
'<div id="thx_ff">&nbsp;</div>'
@@ -921,14 +922,16 @@ function playpause(e) {
var mpui = (function () {
var r = {},
nth = 0,
timeout = null,
preloaded = null;
r.progress_updater = function () {
clearTimeout(timeout);
timer.add(updater_impl, true);
};
function updater_impl() {
if (!mp.au) {
widget.paused(true);
timer.rm(updater_impl);
return;
}
@@ -970,9 +973,9 @@ var mpui = (function () {
}
}
if (!mp.au.paused)
timeout = setTimeout(r.progress_updater, 100);
};
if (mp.au.paused)
timer.rm(updater_impl);
}
r.progress_updater();
return r;
})();
@@ -1461,6 +1464,83 @@ function play_linked() {
})();
function sortfiles(nodes) {
var sopts = jread('fsort', [["href", 1, ""]]);
try {
var is_srch = false;
if (nodes[0]['rp']) {
is_srch = true;
for (var b = 0, bb = nodes.length; b < bb; b++)
nodes[b].ext = nodes[b].rp.split('.').pop();
for (var b = 0; b < sopts.length; b++)
if (sopts[b][0] == 'href')
sopts[b][0] = 'rp';
}
for (var a = sopts.length - 1; a >= 0; a--) {
var name = sopts[a][0], rev = sopts[a][1], typ = sopts[a][2];
if (!name)
continue;
if (name == 'ts')
typ = 'int';
if (name.indexOf('tags/') === 0) {
name = name.slice(5);
for (var b = 0, bb = nodes.length; b < bb; b++)
nodes[b]._sv = nodes[b].tags[name];
}
else {
for (var b = 0, bb = nodes.length; b < bb; b++) {
var v = nodes[b][name];
if ((v + '').indexOf('<a ') === 0)
v = v.split('>')[1];
else if (name == "href" && v) {
if (v.slice(-1) == '/')
v = '\t' + v;
v = uricom_dec(v)[0]
}
nodes[b]._sv = v;
}
}
var onodes = nodes.map(function (x) { return x; });
nodes.sort(function (n1, n2) {
var v1 = n1._sv,
v2 = n2._sv;
if (v1 === undefined) {
if (v2 === undefined) {
return onodes.indexOf(n1) - onodes.indexOf(n2);
}
return -1 * rev;
}
if (v2 === undefined) return 1 * rev;
var ret = rev * (typ == 'int' ? (v1 - v2) : (v1.localeCompare(v2)));
if (ret === 0)
ret = onodes.indexOf(n1) - onodes.indexOf(n2);
return ret;
});
}
for (var b = 0, bb = nodes.length; b < bb; b++) {
delete nodes[b]._sv;
if (is_srch)
delete nodes[b].ext;
}
}
catch (ex) {
console.log("failed to apply sort config: " + ex);
console.log("resetting fsort " + sread('fsort'))
localStorage.removeItem('fsort');
}
return nodes;
}
function fmt_ren(re, md, fmt) {
var ptr = 0;
@@ -1578,7 +1658,7 @@ var fileman = (function () {
r.clip = jread('fman_clip', []);
var nsel = msel.getsel().length;
clmod(bren, 'en', nsel == 1);
clmod(bren, 'en', nsel);
clmod(bdel, 'en', nsel);
clmod(bcut, 'en', nsel);
clmod(bpst, 'en', r.clip && r.clip.length);
@@ -1939,17 +2019,21 @@ var fileman = (function () {
if (!sel.length)
return toast.err(3, 'select at least 1 item to cut');
if (sel.length < 100)
for (var a = 0; a < sel.length; a++) {
vps.push(sel[a].vp);
var cl = ebi(sel[a].id).closest('tr').classList,
inv = cl.contains('c1');
cl.remove(inv ? 'c1' : 'c2');
cl.add(inv ? 'c2' : 'c1');
var els = [];
for (var a = 0; a < sel.length; a++) {
vps.push(sel[a].vp);
if (sel.length < 100) {
els.push(ebi(sel[a].id).closest('tr'));
clmod(els[a], 'fcut');
}
}
toast.inf(1, 'cut ' + sel.length + ' items');
setTimeout(function () {
for (var a = 0; a < els.length; a++)
clmod(els[a], 'fcut', 1);
}, 1);
toast.inf(1.5, 'cut ' + sel.length + ' items');
jwrite('fman_clip', vps);
r.tx(1);
};
@@ -2025,18 +2109,21 @@ var fileman = (function () {
}, null);
};
function onmsg(msg) {
r.clip = null;
r.render();
if (msg == get_evpath())
treectl.goto(msg);
}
if (r.bus)
r.bus.onmessage = function (e) {
r.clip = null;
r.render();
var me = get_evpath();
if (e && e.data == me)
treectl.goto(e.data);
onmsg(e ? e.data : 1)
};
r.tx = function (msg) {
if (!r.bus)
return;
return onmsg(msg);
r.bus.postMessage(msg);
r.bus.onmessage();
@@ -2161,7 +2248,7 @@ var thegrid = (function () {
var oth = ebi(this.getAttribute('ref')),
href = this.getAttribute('href'),
aplay = ebi('a' + oth.getAttribute('id')),
is_img = /\.(gif|jpe?g|png|webp)(\?|$)/i.test(href),
is_img = /\.(gif|jpe?g|png|webp|webm|mp4)(\?|$)/i.test(href),
in_tree = null,
have_sel = QS('#files tr.sel'),
td = oth.closest('td').nextSibling,
@@ -2309,7 +2396,8 @@ var thegrid = (function () {
})();
function tree_scrollto() {
function tree_scrollto(e) {
ev(e);
var act = QS('#treeul a.hl'),
ul = act ? act.offsetParent : null;
@@ -2769,7 +2857,7 @@ var treectl = (function () {
var treectl = {
"hidden": true,
"ls_cb": null,
"dir_cb": null
"dir_cb": tree_scrollto
},
entreed = false,
fixedpos = false,
@@ -2822,7 +2910,7 @@ var treectl = (function () {
}
function onscroll() {
if (!entreed || treectl.hidden)
if (!entreed || treectl.hidden || document.visibilityState == 'hidden')
return;
var tree = ebi('tree'),
@@ -2858,12 +2946,7 @@ var treectl = (function () {
tree.style.height = treeh < 10 ? '' : treeh + 'px';
}
}
function periodic() {
onscroll();
setTimeout(periodic, document.visibilityState ? 100 : 5000);
}
periodic();
timer.add(onscroll, true);
function onresize(e) {
if (!entreed || treectl.hidden)
@@ -3078,7 +3161,13 @@ var treectl = (function () {
}
html.push('</tbody>');
html = html.join('\n');
ebi('files').innerHTML = html;
try {
ebi('files').innerHTML = html;
}
catch (ex) { //ie9
window.location.href = this.top;
return;
}
if (this.hpush)
hist_push(this.top);
@@ -3162,6 +3251,7 @@ var treectl = (function () {
ebi('entree').onclick = treectl.entree;
ebi('detree').onclick = treectl.detree;
ebi('visdir').onclick = tree_scrollto;
ebi('dotfiles').onclick = tdots;
ebi('dyntree').onclick = dyntree;
ebi('twig').onclick = scaletree;
@@ -3698,7 +3788,7 @@ var msel = (function () {
if (r.all && r.all.length) {
for (var a = 0; a < r.all.length; a++) {
var ao = r.all[a];
ao.sel = ebi(ao.id).closest('tr').classList.contains('sel');
ao.sel = clgot(ebi(ao.id).closest('tr'), 'sel');
if (ao.sel)
r.sel.push(ao);
}
@@ -3714,7 +3804,7 @@ var msel = (function () {
item = {};
item.id = links[a].getAttribute('id');
item.sel = links[a].closest('tr').classList.contains('sel');
item.sel = clgot(links[a].closest('tr'), 'sel');
item.vp = href.indexOf('/') !== -1 ? href : vbase + href;
r.all.push(item);

View File

@@ -370,8 +370,8 @@ function save_cb() {
}
if (!r.ok) {
if (!this.btn.classList.contains('force-save')) {
this.btn.classList.add('force-save');
if (!clgot(this.btn, 'force-save')) {
clmod(this.btn, 'force-save', 1);
var msg = [
'This file has been modified since you started editing it!\n',
'if you really want to overwrite, press save again.\n',
@@ -387,7 +387,7 @@ function save_cb() {
return toast.err(0, 'Error! Save failed. Maybe this JSON explains why:\n\n' + this.responseText);
}
this.btn.classList.remove('force-save');
clmod(this.btn, 'force-save');
//alert('save OK -- wrote ' + r.size + ' bytes.\n\nsha512: ' + r.sha512);
run_savechk(r.lastmod, this.txt, this.btn, 0);

View File

@@ -96,20 +96,16 @@ function md_changed(mde, on_srv) {
var md_now = mde.value();
var save_btn = QS('.editor-toolbar button.save');
if (md_now == window.md_saved)
save_btn.classList.add('disabled');
else
save_btn.classList.remove('disabled');
clmod(save_btn, 'disabled', md_now == window.md_saved);
set_jumpto();
}
function save(mde) {
var save_btn = QS('.editor-toolbar button.save');
if (save_btn.classList.contains('disabled'))
if (clgot(save_btn, 'disabled'))
return toast.inf(2, 'no changes');
var force = save_btn.classList.contains('force-save');
var force = clgot(save_btn, 'force-save');
function save2() {
var txt = mde.value();
@@ -153,8 +149,8 @@ function save_cb() {
}
if (!r.ok) {
if (!this.btn.classList.contains('force-save')) {
this.btn.classList.add('force-save');
if (!clgot(this.btn, 'force-save')) {
clmod(this.btn, 'force-save', 1);
var msg = [
'This file has been modified since you started editing it!\n',
'if you really want to overwrite, press save again.\n',
@@ -170,7 +166,7 @@ function save_cb() {
return toast.err(0, 'Error! Save failed. Maybe this JSON explains why:\n\n' + this.responseText);
}
this.btn.classList.remove('force-save');
clmod(this.btn, 'force-save');
//alert('save OK -- wrote ' + r.size + ' bytes.\n\nsha512: ' + r.sha512);
// download the saved doc from the server and compare

View File

@@ -146,6 +146,7 @@ html.light #tt em {
text-align: center;
}
#modalc {
position: relative;
display: inline-block;
background: #f7f7f7;
color: #333;
@@ -165,8 +166,11 @@ html.light #tt em {
}
}
#modalb {
position: sticky;
text-align: right;
padding-top: 1em;
bottom: 0;
right: 0;
}
#modalb a {
color: #000;

View File

@@ -511,7 +511,7 @@ function up2k_init(subtle) {
import_js('/.cpr/deps/' + fn, unmodal);
if (is_https)
ebi('u2foot').innerHTML = shame + ' so <em>this</em> uploader will do like 500kB/s at best';
ebi('u2foot').innerHTML = shame + ' so <em>this</em> uploader will do like 500 KiB/s at best';
else
ebi('u2foot').innerHTML = 'seems like ' + shame + ' so do that if you want more performance <span style="color:#' +
(sha_js == 'ac' ? 'c84">(expecting 20' : '8a5">(but dont worry too much, expect 100') + ' MiB/s)</span>';
@@ -805,13 +805,13 @@ function up2k_init(subtle) {
for (var a = 0; a < good_files.length; a++) {
var fobj = good_files[a][0],
name = good_files[a][1],
fdir = '',
fdir = get_evpath(),
now = Date.now(),
lmod = fobj.lastModified || now,
ofs = name.lastIndexOf('/') + 1;
if (ofs) {
fdir = name.slice(0, ofs);
fdir += url_enc(name.slice(0, ofs));
name = name.slice(ofs);
}
@@ -923,25 +923,23 @@ function up2k_init(subtle) {
}
return handshakes_permitted() && 0 ==
st.todo.handshake.length +
st.busy.handshake.length;
st.busy.handshake.length +
st.todo.upload.length +
st.busy.upload.length;
}
var tasker = (function () {
var tto = null,
running = false,
var running = false,
was_busy = false;
function defer() {
running = false;
clearTimeout(tto);
tto = setTimeout(taskerd, 100);
}
function taskerd() {
if (running)
return;
clearTimeout(tto);
if (crashed)
return defer();
@@ -1032,7 +1030,7 @@ function up2k_init(subtle) {
return defer();
}
}
taskerd();
timer.add(taskerd, true);
return taskerd;
})();
@@ -1265,7 +1263,7 @@ function up2k_init(subtle) {
try { orz(e); } catch (ex) { vis_exh(ex + '', '', '', '', ex); }
};
xhr.open('HEAD', t.purl + t.name, true);
xhr.open('HEAD', t.purl + uricom_enc(t.name), true);
xhr.send();
}
@@ -1342,12 +1340,13 @@ function up2k_init(subtle) {
return;
}
if (response.purl !== t.purl || response.name !== t.name) {
var rsp_purl = url_enc(response.purl);
if (rsp_purl !== t.purl || response.name !== t.name) {
// server renamed us (file exists / path restrictions)
console.log("server-rename [" + t.purl + "] [" + t.name + "] to [" + response.purl + "] [" + response.name + "]");
t.purl = response.purl;
console.log("server-rename [" + t.purl + "] [" + t.name + "] to [" + rsp_purl + "] [" + response.name + "]");
t.purl = rsp_purl;
t.name = response.name;
pvis.seth(t.n, 0, linksplit(t.purl + t.name).join(' '));
pvis.seth(t.n, 0, linksplit(uricom_dec(t.purl)[0] + t.name).join(' '));
}
var chunksize = get_chunksize(t.size),
@@ -1705,11 +1704,10 @@ function up2k_init(subtle) {
catch (ex) { }
try {
var fun = fsearch ? 'add' : 'remove',
ico = fsearch ? '🔎' : '🚀',
var ico = fsearch ? '🔎' : '🚀',
desc = fsearch ? 'Search' : 'Upload';
ebi('op_up2k').classList[fun]('srch');
clmod(ebi('op_up2k'), 'srch', fsearch);
ebi('u2bm').innerHTML = ico + ' <sup>' + desc + '</sup>';
}
catch (ex) { }

View File

@@ -123,11 +123,18 @@ if (!String.startsWith) {
return this.substring(i, i + s.length) === s;
};
}
if (!Element.prototype.matches) {
Element.prototype.matches =
Element.prototype.oMatchesSelector ||
Element.prototype.msMatchesSelector ||
Element.prototype.mozMatchesSelector ||
Element.prototype.webkitMatchesSelector;
}
if (!Element.prototype.closest) {
Element.prototype.closest = function (s) {
var el = this;
do {
if (el.msMatchesSelector(s)) return el;
if (el.matches(s)) return el;
el = el.parentElement || el.parentNode;
} while (el !== null && el.nodeType === 1);
}
@@ -170,90 +177,28 @@ function crc32(str) {
}
function clmod(obj, cls, add) {
function clmod(el, cls, add) {
if (el.classList) {
if (add == 't')
add = el.classList.contains(cls) ? 0 : 1;
return el.classList[add ? 'add' : 'remove'](cls);
}
var re = new RegExp('\\s*\\b' + cls + '\\s*\\b', 'g');
if (add == 't')
add = !re.test(obj.className);
add = !re.test(el.className);
obj.className = obj.className.replace(re, ' ') + (add ? ' ' + cls : '');
el.className = el.className.replace(re, ' ') + (add ? ' ' + cls : '');
}
function sortfiles(nodes) {
var sopts = jread('fsort', [["href", 1, ""]]);
function clgot(el, cls) {
if (el.classList)
return el.classList.contains(cls);
try {
var is_srch = false;
if (nodes[0]['rp']) {
is_srch = true;
for (var b = 0, bb = nodes.length; b < bb; b++)
nodes[b].ext = nodes[b].rp.split('.').pop();
for (var b = 0; b < sopts.length; b++)
if (sopts[b][0] == 'href')
sopts[b][0] = 'rp';
}
for (var a = sopts.length - 1; a >= 0; a--) {
var name = sopts[a][0], rev = sopts[a][1], typ = sopts[a][2];
if (!name)
continue;
if (name == 'ts')
typ = 'int';
if (name.indexOf('tags/') === 0) {
name = name.slice(5);
for (var b = 0, bb = nodes.length; b < bb; b++)
nodes[b]._sv = nodes[b].tags[name];
}
else {
for (var b = 0, bb = nodes.length; b < bb; b++) {
var v = nodes[b][name];
if ((v + '').indexOf('<a ') === 0)
v = v.split('>')[1];
else if (name == "href" && v) {
if (v.slice(-1) == '/')
v = '\t' + v;
v = uricom_dec(v)[0]
}
nodes[b]._sv = v;
}
}
var onodes = nodes.map(function (x) { return x; });
nodes.sort(function (n1, n2) {
var v1 = n1._sv,
v2 = n2._sv;
if (v1 === undefined) {
if (v2 === undefined) {
return onodes.indexOf(n1) - onodes.indexOf(n2);
}
return -1 * rev;
}
if (v2 === undefined) return 1 * rev;
var ret = rev * (typ == 'int' ? (v1 - v2) : (v1.localeCompare(v2)));
if (ret === 0)
ret = onodes.indexOf(n1) - onodes.indexOf(n2);
return ret;
});
}
for (var b = 0, bb = nodes.length; b < bb; b++) {
delete nodes[b]._sv;
if (is_srch)
delete nodes[b].ext;
}
}
catch (ex) {
console.log("failed to apply sort config: " + ex);
console.log("resetting fsort " + sread('fsort'))
localStorage.removeItem('fsort');
}
return nodes;
var lst = (el.getAttribute('class') + '').split(/ /g);
return has(lst, cls);
}
@@ -386,6 +331,16 @@ function uricom_enc(txt, do_fb_enc) {
}
}
function url_enc(txt) {
var parts = txt.split('/'),
ret = [];
for (var a = 0; a < parts.length; a++)
ret.push(uricom_enc(parts[a]));
return ret.join('/');
}
function uricom_dec(txt) {
try {
@@ -555,6 +510,39 @@ function hist_replace(url) {
}
var timer = (function () {
var r = {};
r.q = [];
r.last = 0;
r.add = function (fun, run) {
r.rm(fun);
r.q.push(fun);
if (run)
fun();
};
r.rm = function (fun) {
apop(r.q, fun);
};
function doevents() {
if (Date.now() - r.last < 69)
return;
var q = r.q.slice(0);
for (var a = 0; a < q.length; a++)
q[a]();
r.last = Date.now();
}
setInterval(doevents, 100);
return r;
})();
var tt = (function () {
var r = {
"tt": mknod("div"),
@@ -820,3 +808,14 @@ var modal = (function () {
return r;
})();
function winpopup(txt) {
fetch(get_evpath(), {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
},
body: 'msg=' + uricom_enc(Date.now() + ', ' + txt)
});
}

View File

@@ -173,7 +173,7 @@ about:config >> devtools.debugger.prefs-schema-version = -1
git pull; git reset --hard origin/HEAD && git log --format=format:"%H %ai %d" --decorate=full > ../revs && cat ../{util,browser,up2k}.js >../vr && cat ../revs | while read -r rev extra; do (git reset --hard $rev >/dev/null 2>/dev/null && dsz=$(cat copyparty/web/{util,browser,up2k}.js >../vg 2>/dev/null && diff -wNarU0 ../{vg,vr} | wc -c) && printf '%s %6s %s\n' "$rev" $dsz "$extra") </dev/null; done
# download all sfx versions
curl https://api.github.com/repos/9001/copyparty/releases?per_page=100 | jq -r '.[] | .tag_name + " " + .name' | while read v t; do fn="copyparty $v $t.py"; [ -e $fn ] || curl https://github.com/9001/copyparty/releases/download/$v/copyparty-sfx.py -Lo "$fn"; done
curl https://api.github.com/repos/9001/copyparty/releases?per_page=100 | jq -r '.[] | .tag_name + " " + .name' | tr -d '\r' | while read v t; do fn="copyparty $v $t.py"; [ -e "$fn" ] || curl https://github.com/9001/copyparty/releases/download/$v/copyparty-sfx.py -Lo "$fn"; done
##

View File

@@ -239,7 +239,8 @@ awk '/^LOREM_IPSUM_WORDS/{o=1;print "LOREM_IPSUM_WORDS = u\"a\"";next} !o; /"""/
tmv "$f"
# up2k goes from 28k to 22k laff
echo entabbening
awk 'BEGIN{gensub(//,"",1)}' </dev/null &&
echo entabbening &&
find | grep -E '\.css$' | while IFS= read -r f; do
awk '{
sub(/^[ \t]+/,"");
@@ -253,6 +254,7 @@ find | grep -E '\.css$' | while IFS= read -r f; do
' <$f | sed 's/;\}$/}/' >t
tmv "$f"
done
unexpand -h 2>/dev/null &&
find | grep -E '\.(js|html)$' | while IFS= read -r f; do
unexpand -t 4 --first-only <"$f" >t
tmv "$f"

View File

@@ -364,7 +364,7 @@ def confirm(rv):
except:
pass
sys.exit(rv)
sys.exit(rv or 1)
def run(tmp, j2):

View File

@@ -108,6 +108,7 @@ def tc1():
pdirs = [x.replace("\\", "/") for x in pdirs]
udirs = [x.split("/", 2)[2] for x in pdirs]
perms = [x.rstrip("j/")[-1] for x in pdirs]
perms = ["rw" if x == "a" else x for x in perms]
for pd, ud, p in zip(pdirs, udirs, perms):
if ud[-1] == "j":
continue
@@ -147,14 +148,14 @@ def tc1():
u = "{}{}/a.h264".format(ub, d)
r = requests.get(u)
ok = bool(r)
if ok != (p in ["a"]):
if ok != (p in ["rw"]):
raise Exception("get {} with perm {} at {}".format(ok, p, u))
# stat filesystem
for d, p in zip(pdirs, perms):
u = "{}/a.h264".format(d)
ok = os.path.exists(u)
if ok != (p in ["a", "w"]):
if ok != (p in ["rw", "w"]):
raise Exception("stat {} with perm {} at {}".format(ok, p, u))
# GET thumbnail, vreify contents
@@ -162,7 +163,7 @@ def tc1():
u = "{}{}/a.h264?th=j".format(ub, d)
r = requests.get(u)
ok = bool(r and r.content[:3] == b"\xff\xd8\xff")
if ok != (p in ["a"]):
if ok != (p in ["rw"]):
raise Exception("thumb {} with perm {} at {}".format(ok, p, u))
# check tags
@@ -179,10 +180,10 @@ def tc1():
r_ok = bool(j)
w_ok = bool(r_ok and j.get("files"))
if not r_ok or w_ok != (p in ["a"]):
if not r_ok or w_ok != (p in ["rw"]):
raise Exception("ls {} with perm {} at {}".format(ok, p, u))
if (tag and p != "a") or (not tag and p == "a"):
if (tag and p != "rw") or (not tag and p == "rw"):
raise Exception("tag {} with perm {} at {}".format(tag, p, u))
if tag is not None and tag != "48x32":

View File

@@ -39,6 +39,7 @@ class Cfg(Namespace):
no_scandir=False,
no_sendfile=True,
no_rescan=True,
re_maxage=0,
ihead=False,
nih=True,
mtp=[],

View File

@@ -26,6 +26,7 @@ class Cfg(Namespace):
"no_hash": False,
"css_browser": None,
"no_voldump": True,
"re_maxage": 0,
"rproxy": 0,
}
ex.update(ex2)