Compare commits

...

85 Commits

Author SHA1 Message Date
ed
ec73094506 v0.11.26 2021-06-25 03:10:43 +02:00
ed
c7650c9326 v0.11.25 2021-06-25 03:06:15 +02:00
ed
d94c6d4e72 more rice 2021-06-25 03:02:04 +02:00
ed
3cc8760733 clear seekbar when switching folders 2021-06-25 02:56:21 +02:00
ed
a2f6973495 heh 2021-06-25 02:43:47 +02:00
ed
f8648fa651 always set mediasession play/pause state 2021-06-25 02:39:39 +02:00
ed
177aa038df send charset=utf8 for css, js files 2021-06-25 02:10:42 +02:00
ed
e0a14ec881 event hints for ogvjs playback 2021-06-25 02:03:18 +02:00
ed
9366512f2f audio player: add pause-fade + track-restart +
fix ogvjs paused-seek
2021-06-25 01:46:30 +02:00
ed
ea38b8041a actually fix autoplay on some chromes 2021-06-25 00:43:58 +02:00
ed
f1870daf0d retry filesearch when rate-limited 2021-06-23 22:01:06 +02:00
ed
9722441aad maybe fix autoplay on some chromes 2021-06-23 20:35:05 +02:00
ed
9d014087f4 censor passwords in logs 2021-06-23 00:04:11 +02:00
ed
83b4038b85 ok they actually served a purpose 2021-06-22 21:33:11 +00:00
ed
1e0a448feb audio-key: truncate at 5min + mojibake support 2021-06-22 22:21:39 +02:00
ed
fb81de3b36 v0.11.24 2021-06-22 17:28:09 +02:00
ed
aa4f352301 prefer audio tags in audio files 2021-06-22 17:21:24 +02:00
ed
f1a1c2ea45 recover from opening a corrupt database 2021-06-22 17:19:56 +02:00
ed
6249bd4163 add pebkac hints 2021-06-22 17:18:34 +02:00
ed
2579dc64ce update notes 2021-06-21 22:49:28 +00:00
ed
356512270a file extensions dont contain whitespace 2021-06-21 23:50:35 +02:00
ed
bed27f2b43 mention fix for the OSD popup on windows 2021-06-21 23:43:07 +02:00
ed
54013d861b v0.11.23 2021-06-21 21:15:56 +02:00
ed
ec100210dc support showing album-cover on windows lockscreen 2021-06-21 19:15:22 +00:00
ed
3ab1acf32c v0.11.22 2021-06-21 20:30:29 +02:00
ed
8c28266418 subscribe to media-keys globally as a media player 2021-06-21 20:26:11 +02:00
ed
7f8b8dcb92 scandir is not withable before py3.6 2021-06-21 20:23:35 +02:00
ed
6dd39811d4 disable u2idx if sqlite3 is unavailable 2021-06-21 20:22:54 +02:00
ed
35e2138e3e doc: macos support 2021-06-21 18:42:15 +02:00
ed
239b4e9fe6 v0.11.21 2021-06-20 21:25:18 +02:00
ed
2fcd0e7e72 abandon listing tags in browser when db busy 2021-06-20 21:19:47 +02:00
ed
357347ce3a lower timeout on db reads 2021-06-20 21:03:35 +02:00
ed
36dc1107fb update dbtool desc 2021-06-20 20:05:43 +02:00
ed
0a3bbc4b4a v0.11.20 for real 2021-06-20 19:32:17 +02:00
ed
855b93dcf6 v0.11.20 2021-06-20 18:53:58 +02:00
ed
89b79ba267 fix histpath getting indexed on windows 2021-06-20 17:59:27 +02:00
ed
f5651b7d94 dont include hidden colums in /np clips 2021-06-20 17:45:59 +02:00
ed
1881019ede support cygpaths for mtag binaries 2021-06-20 17:45:23 +02:00
ed
caba4e974c upgrade dbtool for v4 2021-06-20 17:44:24 +02:00
ed
bc3c9613bc cosmetic macos fix on shutdown 2021-06-20 15:50:37 +02:00
ed
15a3ee252e support backslash in filenames 2021-06-20 15:50:06 +02:00
ed
be055961ae adjust up2k hashlen to match base64 window 2021-06-20 15:32:36 +02:00
ed
e3031bdeec fix up2k folder-upload 2021-06-20 00:00:50 +00:00
ed
75917b9f7c better fallback 2021-06-19 16:21:39 +02:00
ed
910732e02c update build notes 2021-06-19 16:20:35 +02:00
ed
264b497681 v0.11.19 2021-06-19 01:32:17 +02:00
ed
372b949622 fix tooltip indicator 2021-06-19 01:25:07 +02:00
ed
789a602914 save some more bytes on the wire 2021-06-19 01:18:48 +02:00
ed
093e955100 move stuff that needs javascript out of the html 2021-06-19 01:10:40 +02:00
ed
c32a89bebf minor lightmode tweaks 2021-06-19 00:17:39 +02:00
ed
c0bebe9f9f eq-param error-hilight in lightmode 2021-06-18 23:51:26 +02:00
ed
57579b2fe5 fix android-chrome layout glitch in up2k 2021-06-18 23:38:43 +02:00
ed
51d14a6b4d fix toolbar tooltips on android 2021-06-18 22:11:01 +02:00
ed
c50f1b64e5 dodge android-chrome bug: canvas aspect ratio 2021-06-18 21:46:15 +02:00
ed
98aaab02c5 block scroll events, hilight selected radios 2021-06-18 20:49:38 +02:00
ed
0fc7973d8b add shadow to playback times 2021-06-18 20:24:36 +02:00
ed
10362aa02e v0.11.18 2021-06-18 00:30:37 +02:00
ed
0a8e759fe6 v0.11.17 2021-06-17 00:31:38 +02:00
ed
d70981cdd1 fix eq param input 2021-06-17 00:29:14 +02:00
ed
e08c03b886 audio-filters: expose gain control 2021-06-16 22:25:29 +02:00
ed
56086e8984 ux: contrast tweaks + fix anchor-scroll 2021-06-16 21:38:30 +02:00
ed
1aa9033022 add play/pause hotkey 2021-06-16 19:19:29 +02:00
ed
076e103d53 ux: responsive settings layout 2021-06-16 19:10:32 +02:00
ed
38c00ea8fc print thumbnail cleanup summary 2021-06-16 18:57:10 +02:00
ed
415757af43 mention the symlink-scanner too 2021-06-16 18:37:23 +02:00
ed
e72ed8c0ed mention some essentials 2021-06-16 18:29:29 +02:00
ed
32f9c6b5bb v0.11.16 2021-06-16 01:51:18 +02:00
ed
6251584ef6 fix .13dB clipping with all-zero eq 2021-06-15 23:37:44 +00:00
ed
f3e413bc28 icons 2021-06-16 00:01:07 +02:00
ed
6f6cc8f3f8 move eq to the player settings tab 2021-06-15 22:26:39 +02:00
ed
8b081e9e69 media player: continue to next folder 2021-06-15 22:19:53 +02:00
ed
c8a510d10e fully hide columns when minimized 2021-06-15 21:43:37 +02:00
ed
6f834f6679 sticky tree header 2021-06-15 21:07:27 +02:00
ed
cf2d6650ac audio-eq: flatten frequency response 2021-06-15 21:06:00 +02:00
ed
cd52dea488 v0.11.15 2021-06-15 00:01:11 +02:00
ed
6ea75df05d add audio equalizer 2021-06-14 23:58:56 +02:00
ed
4846e1e8d6 mention num.clients for rproxy 2021-06-14 19:27:34 +02:00
ed
fc024f789d v0.11.14 2021-06-14 03:05:50 +02:00
ed
473e773aea fix deadlock 2021-06-14 00:55:11 +00:00
ed
48a2e1a353 add threadwatcher 2021-06-14 01:57:18 +02:00
ed
6da63fbd79 up2k-cli: recover from lost handshakes 2021-06-14 01:01:06 +02:00
ed
5bec37fcee fix cosmetic login glitch 2021-06-14 00:28:08 +02:00
ed
3fd0ba0a31 oh right its the other way around 2021-06-13 22:49:55 +02:00
ed
241a143366 add --rproxy for explicit proxy level 2021-06-13 22:22:31 +02:00
ed
a537064da7 custom-css example to add filetype icons 2021-06-13 00:49:28 +02:00
43 changed files with 2577 additions and 1023 deletions

View File

@@ -51,8 +51,10 @@ turn your phone or raspi into a portable file server with resumable uploads/down
* [sfx](#sfx)
* [sfx repack](#sfx-repack)
* [install on android](#install-on-android)
* [dev env setup](#dev-env-setup)
* [how to release](#how-to-release)
* [building](#building)
* [dev env setup](#dev-env-setup)
* [just the sfx](#just-the-sfx)
* [complete release](#complete-release)
* [todo](#todo)
@@ -62,6 +64,14 @@ download [copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/do
running the sfx without arguments (for example doubleclicking it on Windows) will give everyone full access to the current folder; see `-h` for help if you want accounts and volumes etc
some recommended options:
* `-e2dsa` enables general file indexing, see [search configuration](#search-configuration)
* `-e2ts` enables audio metadata indexing (needs either FFprobe or mutagen), see [optional dependencies](#optional-dependencies)
* `-v /mnt/music:/music:r:afoo -a foo:bar` shares `/mnt/music` as `/music`, `r`eadable by anyone, with user `foo` as `a`dmin (read/write), password `bar`
* replace `:r:afoo` with `:rfoo` to only make the folder readable by `foo` and nobody else
* in addition to `r`ead and `a`dmin, `w`rite makes a folder write-only, so cannot list/access files in it
* `--ls '**,*,ln,p,r'` to crash on startup if any of the volumes contain a symlink which point outside the volume, as that could give users unintended access
you may also want these, especially on servers:
* [contrib/systemd/copyparty.service](contrib/systemd/copyparty.service) to run copyparty as a systemd service
* [contrib/nginx/copyparty.conf](contrib/nginx/copyparty.conf) to reverse-proxy behind nginx (for better https)
@@ -101,7 +111,7 @@ summary: all planned features work! now please enjoy the bloatening
* ☑ FUSE client (read-only)
* browser
* ☑ tree-view
*media player
*audio player (with OS media controls)
* ☑ thumbnails
* ☑ images using Pillow
* ☑ videos using FFmpeg
@@ -133,6 +143,9 @@ summary: all planned features work! now please enjoy the bloatening
## not my bugs
* Windows: folders cannot be accessed if the name ends with `.`
* python or windows bug
* Windows: msys2-python 3.8.6 occasionally throws "RuntimeError: release unlocked lock" when leaving a scoped mutex in up2k
* this is an msys2 bug, the regular windows edition of python is fine
@@ -155,15 +168,16 @@ summary: all planned features work! now please enjoy the bloatening
## hotkeys
the browser has the following hotkeys
* `B` toggle breadcrumbs / directory tree
* `I/K` prev/next folder
* `P` parent folder
* `M` parent folder
* `G` toggle list / grid view
* `T` toggle thumbnails / icons
* when playing audio:
* `0..9` jump to 10%..90%
* `U/O` skip 10sec back/forward
* `J/L` prev/next song
* `J` also starts playing the folder
* `P` play/pause (also starts playing the folder)
* in the grid view:
* `S` toggle multiselect
* `A/D` zoom
@@ -171,9 +185,9 @@ the browser has the following hotkeys
## tree-mode
by default there's a breadcrumbs path; you can replace this with a tree-browser sidebar thing by clicking the 🌲
by default there's a breadcrumbs path; you can replace this with a tree-browser sidebar thing by clicking the `🌲` or pressing the `B` hotkey
click `[-]` and `[+]` to adjust the size, and the `[a]` toggles if the tree should widen dynamically as you go deeper or stay fixed-size
click `[-]` and `[+]` (or hotkeys `A`/`D`) to adjust the size, and the `[a]` toggles if the tree should widen dynamically as you go deeper or stay fixed-size
## thumbnails
@@ -267,6 +281,8 @@ up2k has saved a few uploads from becoming corrupted in-transfer already; caught
* you can link a particular timestamp in an audio file by adding it to the URL, such as `&20` / `&20s` / `&1m20` / `&t=1:20` after the `.../#af-c8960dab`
* if you are using media hotkeys to switch songs and are getting tired of seeing the OSD popup which Windows doesn't let you disable, consider https://ocv.me/dev/?media-osd-bgone.ps1
# searching
@@ -301,7 +317,7 @@ the same arguments can be set as volume flags, in addition to `d2d` and `d2t` fo
* `-v ~/music::r:cd2t` disables all `-e2t*` (tags), does not affect `-e2d*`
note:
* `e2tsr` is probably always overkill, since `e2ds`/`e2dsa` would pick up any file modifications and cause `e2ts` to reindex those
* `e2tsr` is probably always overkill, since `e2ds`/`e2dsa` would pick up any file modifications and `e2ts` would then reindex those
* the rescan button in the admin panel has no effect unless the volume has `-e2ds` or higher
you can choose to only index filename/path/size/last-modified (and not the hash of the file contents) by setting `--no-hash` or the volume-flag `cdhash`, this has the following consequences:
@@ -429,7 +445,7 @@ quick summary of more eccentric web-browsers trying to view a directory index:
copyparty returns a truncated sha512sum of your PUT/POST as base64; you can generate the same checksum locally to verify uplaods:
b512(){ printf "$((sha512sum||shasum -a512)|sed -E 's/ .*//;s/(..)/\\x\1/g')"|base64|head -c43;}
b512(){ printf "$((sha512sum||shasum -a512)|sed -E 's/ .*//;s/(..)/\\x\1/g')"|base64|tr '+/' '-_'|head -c44;}
b512 <movie.mkv
@@ -524,18 +540,45 @@ echo $?
after the initial setup, you can launch copyparty at any time by running `copyparty` anywhere in Termux
# dev env setup
# building
## dev env setup
mostly optional; if you need a working env for vscode or similar
```sh
python3 -m venv .venv
. .venv/bin/activate
pip install jinja2 # mandatory deps
pip install Pillow # thumbnail deps
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
```
# how to release
## just the sfx
unless you need to modify something in the web-dependencies, it's faster to grab those from a previous release:
```sh
rm -rf copyparty/web/deps
curl -L https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py >x.py
python3 x.py -h
rm x.py
mv /tmp/pe-copyparty/copyparty/web/deps/ copyparty/web/
```
then build the sfx using any of the following examples:
```sh
./scripts/make-sfx.sh # both python and sh editions
./scripts/make-sfx.sh no-sh gz # just python with gzip
```
## complete release
also builds the sfx so disregard the sfx section above
in the `scripts` folder:

View File

@@ -48,15 +48,16 @@ you could replace winfsp with [dokan](https://github.com/dokan-dev/dokany/releas
# [`dbtool.py`](dbtool.py)
upgrade utility which can show db info and help transfer data between databases, for example when a new version of copyparty recommends to wipe the DB and reindex because it now collects additional metadata during analysis, but you have some really expensive `-mtp` parsers and want to copy over the tags from the old db
upgrade utility which can show db info and help transfer data between databases, for example when a new version of copyparty is incompatible with the old DB and automatically rebuilds the DB from scratch, but you have some really expensive `-mtp` parsers and want to copy over the tags from the old db
for that example (upgrading to v0.11.0), first move the old db aside, launch copyparty, let it rebuild the db until the point where it starts running mtp (colored messages as it adds the mtp tags), then CTRL-C and patch in the old mtp tags from the old db instead
for that example (upgrading to v0.11.20), first launch the new version of copyparty like usual, let it make a backup of the old db and rebuild the new db until the point where it starts running mtp (colored messages as it adds the mtp tags), that's when you hit CTRL-C and patch in the old mtp tags from the old db instead
so assuming you have `-mtp` parsers to provide the tags `key` and `.bpm`:
```
~/bin/dbtool.py -ls up2k.db
~/bin/dbtool.py -src up2k.db.v0.10.22 up2k.db -cmp
~/bin/dbtool.py -src up2k.db.v0.10.22 up2k.db -rm-mtp-flag -copy key
~/bin/dbtool.py -src up2k.db.v0.10.22 up2k.db -rm-mtp-flag -copy .bpm -vac
cd /mnt/nas/music/.hist
~/src/copyparty/bin/dbtool.py -ls up2k.db
~/src/copyparty/bin/dbtool.py -src up2k.*.v3 up2k.db -cmp
~/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
```

View File

@@ -2,10 +2,13 @@
import os
import sys
import time
import shutil
import sqlite3
import argparse
DB_VER = 3
DB_VER1 = 3
DB_VER2 = 4
def die(msg):
@@ -45,18 +48,21 @@ def compare(n1, d1, n2, d2, verbose):
nt = next(d1.execute("select count(w) from up"))[0]
n = 0
miss = 0
for w, rd, fn in d1.execute("select w, rd, fn from up"):
for w1, rd, fn in d1.execute("select w, rd, fn from up"):
n += 1
if n % 25_000 == 0:
m = f"\033[36mchecked {n:,} of {nt:,} files in {n1} against {n2}\033[0m"
print(m)
q = "select w from up where substr(w,1,16) = ?"
hit = d2.execute(q, (w[:16],)).fetchone()
if rd.split("/", 1)[0] == ".hist":
continue
q = "select w from up where rd = ? and fn = ?"
hit = d2.execute(q, (rd, fn)).fetchone()
if not hit:
miss += 1
if verbose:
print(f"file in {n1} missing in {n2}: [{w}] {rd}/{fn}")
print(f"file in {n1} missing in {n2}: [{w1}] {rd}/{fn}")
print(f" {miss} files in {n1} missing in {n2}\n")
@@ -64,15 +70,30 @@ def compare(n1, d1, n2, d2, verbose):
n = 0
miss = {}
nmiss = 0
for w, k, v in d1.execute("select * from mt"):
for w1, k, v in d1.execute("select * from mt"):
n += 1
if n % 100_000 == 0:
m = f"\033[36mchecked {n:,} of {nt:,} tags in {n1} against {n2}, so far {nmiss} missing tags\033[0m"
print(m)
v2 = d2.execute("select v from mt where w = ? and +k = ?", (w, k)).fetchone()
if v2:
v2 = v2[0]
q = "select rd, fn from up where substr(w,1,16) = ?"
rd, fn = d1.execute(q, (w1,)).fetchone()
if rd.split("/", 1)[0] == ".hist":
continue
q = "select substr(w,1,16) from up where rd = ? and fn = ?"
w2 = d2.execute(q, (rd, fn)).fetchone()
if w2:
w2 = w2[0]
v2 = None
if w2:
v2 = d2.execute(
"select v from mt where w = ? and +k = ?", (w2, k)
).fetchone()
if v2:
v2 = v2[0]
# if v != v2 and v2 and k in [".bpm", "key"] and n2 == "src":
# print(f"{w} [{rd}/{fn}] {k} = [{v}] / [{v2}]")
@@ -99,9 +120,7 @@ def compare(n1, d1, n2, d2, verbose):
miss[k] = 1
if verbose:
q = "select rd, fn from up where substr(w,1,16) = ?"
rd, fn = d1.execute(q, (w,)).fetchone()
print(f"missing in {n2}: [{w}] [{rd}/{fn}] {k} = {v}")
print(f"missing in {n2}: [{w1}] [{rd}/{fn}] {k} = {v}")
for k, v in sorted(miss.items()):
if v:
@@ -114,24 +133,35 @@ def copy_mtp(d1, d2, tag, rm):
nt = next(d1.execute("select count(w) from mt where k = ?", (tag,)))[0]
n = 0
ndone = 0
for w, k, v in d1.execute("select * from mt where k = ?", (tag,)):
for w1, k, v in d1.execute("select * from mt where k = ?", (tag,)):
n += 1
if n % 25_000 == 0:
m = f"\033[36m{n:,} of {nt:,} tags checked, so far {ndone} copied\033[0m"
print(m)
hit = d2.execute("select v from mt where w = ? and +k = ?", (w, k)).fetchone()
q = "select rd, fn from up where substr(w,1,16) = ?"
rd, fn = d1.execute(q, (w1,)).fetchone()
if rd.split("/", 1)[0] == ".hist":
continue
q = "select substr(w,1,16) from up where rd = ? and fn = ?"
w2 = d2.execute(q, (rd, fn)).fetchone()
if not w2:
continue
w2 = w2[0]
hit = d2.execute("select v from mt where w = ? and +k = ?", (w2, k)).fetchone()
if hit:
hit = hit[0]
if hit != v:
ndone += 1
if hit is not None:
d2.execute("delete from mt where w = ? and +k = ?", (w, k))
d2.execute("delete from mt where w = ? and +k = ?", (w2, k))
d2.execute("insert into mt values (?,?,?)", (w, k, v))
d2.execute("insert into mt values (?,?,?)", (w2, k, v))
if rm:
d2.execute("delete from mt where w = ? and +k = 't:mtp'", (w,))
d2.execute("delete from mt where w = ? and +k = 't:mtp'", (w2,))
d2.commit()
print(f"copied {ndone} {tag} tags over")
@@ -140,7 +170,7 @@ def copy_mtp(d1, d2, tag, rm):
def main():
os.system("")
print()
ap = argparse.ArgumentParser()
ap.add_argument("db", help="database to work on")
ap.add_argument("-src", metavar="DB", type=str, help="database to copy from")
@@ -168,6 +198,23 @@ def main():
db = sqlite3.connect(ar.db)
ds = sqlite3.connect(ar.src) if ar.src else None
# revert journals
for d, p in [[db, ar.db], [ds, ar.src]]:
if not d:
continue
pj = "{}-journal".format(p)
if not os.path.exists(pj):
continue
d.execute("create table foo (bar int)")
d.execute("drop table foo")
if ar.copy:
db.close()
shutil.copy2(ar.db, "{}.bak.dbtool.{:x}".format(ar.db, int(time.time())))
db = sqlite3.connect(ar.db)
for d, n in [[ds, "src"], [db, "dst"]]:
if not d:
continue
@@ -176,8 +223,8 @@ def main():
if ver == "corrupt":
die("{} database appears to be corrupt, sorry")
if ver != DB_VER:
m = f"{n} db is version {ver}, this tool only supports version {DB_VER}, please upgrade it with copyparty first"
if ver < DB_VER1 or ver > DB_VER2:
m = f"{n} db is version {ver}, this tool only supports versions between {DB_VER1} and {DB_VER2}, please upgrade it with copyparty first"
die(m)
if ar.ls:

View File

@@ -60,7 +60,7 @@ def main():
try:
det(tf)
except:
pass
pass # mute
finally:
os.unlink(tf)

123
bin/mtag/audio-key-slicing.py Executable file
View File

@@ -0,0 +1,123 @@
#!/usr/bin/env python
import re
import os
import sys
import tempfile
import subprocess as sp
import keyfinder
from copyparty.util import fsenc
"""
dep: github/mixxxdj/libkeyfinder
dep: pypi/keyfinder
dep: ffmpeg
note: this is a janky edition of the regular audio-key.py,
slicing the files at 20sec intervals and keeping 5sec from each,
surprisingly accurate but still garbage (446 ok, 69 bad, 13% miss)
it is fast tho
"""
def get_duration():
# TODO provide ffprobe tags to mtp as json
# fmt: off
dur = sp.check_output([
"ffprobe",
"-hide_banner",
"-v", "fatal",
"-show_streams",
"-show_format",
fsenc(sys.argv[1])
])
# fmt: on
dur = dur.decode("ascii", "replace").split("\n")
dur = [x.split("=")[1] for x in dur if x.startswith("duration=")]
dur = [float(x) for x in dur if re.match(r"^[0-9\.,]+$", x)]
return list(sorted(dur))[-1] if dur else None
def get_segs(dur):
# keep first 5s of each 20s,
# keep entire last segment
ofs = 0
segs = []
while True:
seg = [ofs, 5]
segs.append(seg)
if dur - ofs < 20:
seg[-1] = int(dur - seg[0])
break
ofs += 20
return segs
def slice(tf):
dur = get_duration()
dur = min(dur, 600) # max 10min
segs = get_segs(dur)
# fmt: off
cmd = [
"ffmpeg",
"-nostdin",
"-hide_banner",
"-v", "fatal",
"-y"
]
for seg in segs:
cmd.extend([
"-ss", str(seg[0]),
"-i", fsenc(sys.argv[1])
])
filt = ""
for n, seg in enumerate(segs):
filt += "[{}:a:0]atrim=duration={}[a{}]; ".format(n, seg[1], n)
prev = "a0"
for n in range(1, len(segs)):
nxt = "b{}".format(n)
filt += "[{}][a{}]acrossfade=d=0.5[{}]; ".format(prev, n, nxt)
prev = nxt
cmd.extend([
"-filter_complex", filt[:-2],
"-map", "[{}]".format(nxt),
"-sample_fmt", "s16",
tf
])
# fmt: on
# print(cmd)
sp.check_call(cmd)
def det(tf):
slice(tf)
print(keyfinder.key(tf).camelot())
def main():
with tempfile.NamedTemporaryFile(suffix=".flac", delete=False) as f:
f.write(b"h")
tf = f.name
try:
det(tf)
finally:
os.unlink(tf)
pass
if __name__ == "__main__":
main()

View File

@@ -1,18 +1,54 @@
#!/usr/bin/env python
import os
import sys
import tempfile
import subprocess as sp
import keyfinder
from copyparty.util import fsenc
"""
dep: github/mixxxdj/libkeyfinder
dep: pypi/keyfinder
dep: ffmpeg
note: cannot fsenc
"""
try:
print(keyfinder.key(sys.argv[1]).camelot())
except:
pass
# tried trimming the first/last 5th, bad idea,
# misdetects 9a law field (Sphere Caliber) as 10b,
# obvious when mixing 9a ghostly parapara ship
def det(tf):
# fmt: off
sp.check_call([
"ffmpeg",
"-nostdin",
"-hide_banner",
"-v", "fatal",
"-y", "-i", fsenc(sys.argv[1]),
"-t", "300",
"-sample_fmt", "s16",
tf
])
# fmt: on
print(keyfinder.key(tf).camelot())
def main():
with tempfile.NamedTemporaryFile(suffix=".flac", delete=False) as f:
f.write(b"h")
tf = f.name
try:
det(tf)
except:
pass # mute
finally:
os.unlink(tf)
if __name__ == "__main__":
main()

View File

@@ -1,3 +1,8 @@
# when running copyparty behind a reverse-proxy,
# make sure that copyparty allows at least as many clients as the proxy does,
# so run copyparty with -nc 512 if your nginx has the default limits
# (worker_processes 1, worker_connections 512)
upstream cpp {
server 127.0.0.1:3923;
keepalive 120;

View File

@@ -23,7 +23,7 @@ from textwrap import dedent
from .__init__ import E, WINDOWS, VT100, PY2
from .__version__ import S_VERSION, S_BUILD_DT, CODENAME
from .svchub import SvcHub
from .util import py_desc, align_tab, IMPLICATIONS
from .util import py_desc, align_tab, IMPLICATIONS, alltrace
HAVE_SSL = True
try:
@@ -182,6 +182,16 @@ def sighandler(sig=None, frame=None):
print("\n".join(msg))
def stackmon(fp, ival):
ctr = 0
while True:
ctr += 1
time.sleep(ival)
st = "{}, {}\n{}".format(ctr, time.time(), alltrace())
with open(fp, "wb") as f:
f.write(st.encode("utf-8", "replace"))
def run_argparse(argv, formatter):
ap = argparse.ArgumentParser(
formatter_class=formatter,
@@ -222,10 +232,6 @@ def run_argparse(argv, formatter):
"print,get" prints the data in the log and returns GET
(leave out the ",get" to return an error instead)
--ciphers help = available ssl/tls ciphers,
--ssl-ver help = available ssl/tls versions,
default is what python considers safe, usually >= TLS1
values for --ls:
"USR" is a user to browse as; * is anonymous, ** is all users
"VOL" is a single volume to scan, default is * (all vols)
@@ -244,27 +250,45 @@ def run_argparse(argv, formatter):
)
# fmt: off
ap.add_argument("-c", metavar="PATH", type=str, action="append", help="add config file")
ap.add_argument("-i", metavar="IP", type=str, default="0.0.0.0", help="ip to bind (comma-sep.)")
ap.add_argument("-p", metavar="PORT", type=str, default="3923", help="ports to bind (comma/range)")
ap.add_argument("-nc", metavar="NUM", type=int, default=64, help="max num clients")
ap.add_argument("-j", metavar="CORES", type=int, default=1, help="max num cpu cores")
ap.add_argument("-a", metavar="ACCT", type=str, action="append", help="add account")
ap.add_argument("-v", metavar="VOL", type=str, action="append", help="add volume")
ap.add_argument("-q", action="store_true", help="quiet")
ap.add_argument("-a", metavar="ACCT", type=str, action="append", help="add account, USER:PASS; example [ed:wark")
ap.add_argument("-v", metavar="VOL", type=str, action="append", help="add volume, SRC:DST:FLAG; example [.::r], [/mnt/nas/music:/music:r:aed")
ap.add_argument("-ed", action="store_true", help="enable ?dots")
ap.add_argument("-emp", action="store_true", help="enable markdown plugins")
ap.add_argument("-mcr", metavar="SEC", type=int, default=60, help="md-editor mod-chk rate")
ap.add_argument("-nw", action="store_true", help="disable writes (benchmark)")
ap.add_argument("-nih", action="store_true", help="no info hostname")
ap.add_argument("-nid", action="store_true", help="no info disk-usage")
ap.add_argument("--dotpart", action="store_true", help="dotfile incomplete uploads")
ap.add_argument("--no-zip", action="store_true", help="disable download as zip/tar")
ap.add_argument("--sparse", metavar="MiB", type=int, default=4, help="up2k min.size threshold (mswin-only)")
ap.add_argument("--urlform", metavar="MODE", type=str, default="print,get", help="how to handle url-forms")
ap.add_argument("--salt", type=str, default="hunter2", help="up2k file-hash salt")
ap.add_argument("--urlform", metavar="MODE", type=str, default="print,get", help="how to handle url-forms; examples: [stash], [save,get]")
ap2 = ap.add_argument_group('appearance options')
ap2.add_argument("--css-browser", metavar="L", help="URL to additional CSS to include")
ap2 = ap.add_argument_group('network options')
ap2.add_argument("-i", metavar="IP", type=str, default="0.0.0.0", help="ip to bind (comma-sep.)")
ap2.add_argument("-p", metavar="PORT", type=str, 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 = 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("--ssl-ver", metavar="LIST", type=str, help="set allowed ssl/tls versions; [help] shows available versions; default is what your python version considers safe")
ap2.add_argument("--ciphers", metavar="LIST", 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", help="log master secrets")
ap2 = ap.add_argument_group('opt-outs')
ap2.add_argument("-nw", action="store_true", help="disable writes (benchmark)")
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 = ap.add_argument_group('safety options')
ap2.add_argument("--ls", metavar="U[,V[,F]]", help="scan all volumes; arguments USER,VOL,FLAGS; example [**,*,ln,p,r]")
ap2.add_argument("--salt", type=str, default="hunter2", help="up2k file-hash salt")
ap2 = ap.add_argument_group('logging options')
ap2.add_argument("-q", action="store_true", help="quiet")
ap2.add_argument("--log-conn", action="store_true", help="print tcp-server msgs")
ap2.add_argument("--ihead", metavar="HEADER", action='append', help="dump incoming header")
ap2.add_argument("--lf-url", metavar="RE", type=str, default=r"^/\.cpr/|\?th=[wj]$", help="dont log URLs matching")
ap2 = ap.add_argument_group('admin panel options')
ap2.add_argument("--no-rescan", action="store_true", help="disable ?scan (volume reindexing)")
@@ -299,22 +323,14 @@ def run_argparse(argv, formatter):
ap2.add_argument("-mtp", metavar="M=[f,]bin", action="append", type=str, help="read tag M using bin")
ap2.add_argument("--srch-time", metavar="SEC", type=int, default=30, help="search deadline")
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("--ssl-ver", metavar="LIST", type=str, help="ssl/tls versions to allow")
ap2.add_argument("--ciphers", metavar="LIST", help="set allowed ciphers")
ap2.add_argument("--ssl-dbg", action="store_true", help="dump some tls info")
ap2.add_argument("--ssl-log", metavar="PATH", help="log master secrets")
ap2 = ap.add_argument_group('appearance options')
ap2.add_argument("--css-browser", metavar="L", help="URL to additional CSS to include")
ap2 = ap.add_argument_group('debug options')
ap2.add_argument("--ls", metavar="U[,V[,F]]", help="scan all volumes")
ap2.add_argument("--log-conn", action="store_true", help="print tcp-server msgs")
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("--ihead", metavar="HEADER", action='append', help="dump incoming header")
ap2.add_argument("--lf-url", metavar="RE", type=str, default=r"^/\.cpr/|\?th=[wj]$", help="dont log URLs matching")
ap2.add_argument("--stackmon", metavar="P,S", help="write stacktrace to Path every S second")
return ap.parse_args(args=argv[1:])
# fmt: on
@@ -354,6 +370,16 @@ def main(argv=None):
except AssertionError:
al = run_argparse(argv, Dodge11874)
if al.stackmon:
fp, f = al.stackmon.rsplit(",", 1)
f = int(f)
t = threading.Thread(
target=stackmon,
args=(fp, f),
)
t.daemon = True
t.start()
# propagate implications
for k1, k2 in IMPLICATIONS:
if getattr(al, k1):
@@ -384,7 +410,7 @@ def main(argv=None):
+ " (if you crash with codec errors then that is why)"
)
if WINDOWS and sys.version_info < (3, 6):
if sys.version_info < (3, 6):
al.no_scandir = True
# signal.signal(signal.SIGINT, sighandler)

View File

@@ -1,8 +1,8 @@
# coding: utf-8
VERSION = (0, 11, 13)
VERSION = (0, 11, 26)
CODENAME = "the grid"
BUILD_DT = (2021, 6, 12)
BUILD_DT = (2021, 6, 25)
S_VERSION = ".".join(map(str, VERSION))
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)

View File

@@ -10,7 +10,7 @@ import hashlib
import threading
from .__init__ import WINDOWS
from .util import IMPLICATIONS, undot, Pebkac, fsdec, fsenc, statdir, nuprint
from .util import IMPLICATIONS, uncyg, undot, Pebkac, fsdec, fsenc, statdir, nuprint
class VFS(object):
@@ -439,8 +439,8 @@ class AuthSrv(object):
raise Exception("invalid -v argument: [{}]".format(v_str))
src, dst, perms = m.groups()
if WINDOWS and src.startswith("/"):
src = "{}:\\{}".format(src[1], src[3:])
if WINDOWS:
src = uncyg(src)
# print("\n".join([src, dst, perms]))
src = fsdec(os.path.abspath(fsenc(src)))
@@ -469,6 +469,17 @@ class AuthSrv(object):
print(m.format(cfg_fn, self.line_ctr))
raise
# case-insensitive; normalize
if WINDOWS:
cased = {}
for k, v in mount.items():
try:
cased[k] = fsdec(os.path.realpath(fsenc(v)))
except:
cased[k] = v
mount = cased
if not mount:
# -h says our defaults are CWD at root and read/write for everyone
vfs = VFS(os.path.abspath("."), "", ["*"], ["*"])
@@ -524,9 +535,7 @@ class AuthSrv(object):
if vflag == "-":
pass
elif vflag:
if WINDOWS and vflag.startswith("/"):
vflag = "{}:\\{}".format(vflag[1], vflag[3:])
vol.histpath = vflag
vol.histpath = uncyg(vflag) if WINDOWS else vflag
elif self.args.hist:
for nch in range(len(hid)):
hpath = os.path.join(self.args.hist, hid[: nch + 1])
@@ -684,6 +693,9 @@ class AuthSrv(object):
self.user = user
self.iuser = {v: k for k, v in user.items()}
pwds = [re.escape(x) for x in self.iuser.keys()]
self.re_pwd = re.compile("=(" + "|".join(pwds) + ")([]&; ]|$)")
# import pprint
# pprint.pprint({"usr": user, "rd": mread, "wr": mwrite, "mnt": mount})

View File

@@ -68,6 +68,7 @@ class MpWorker(object):
# self.logw("work: [{}]".format(d[0]))
if dest == "shutdown":
self.httpsrv.shutdown()
self.logw("ok bye")
sys.exit(0)
return

View File

@@ -25,6 +25,7 @@ class BrokerThr(object):
def shutdown(self):
# self.log("broker", "shutting down")
self.httpsrv.shutdown()
pass
def put(self, want_retval, dest, *args):

View File

@@ -10,7 +10,6 @@ import json
import string
import socket
import ctypes
import traceback
from datetime import datetime
import calendar
@@ -50,12 +49,21 @@ class HttpCli(object):
self.tls = hasattr(self.s, "cipher")
self.bufsz = 1024 * 32
self.hint = None
self.absolute_urls = False
self.out_headers = {"Access-Control-Allow-Origin": "*"}
def log(self, msg, c=0):
ptn = self.asrv.re_pwd
if ptn.search(msg):
msg = ptn.sub(self.unpwd, msg)
self.log_func(self.log_src, msg, c)
def unpwd(self, m):
a, b = m.groups()
return "=\033[7m {} \033[27m{}".format(self.asrv.iuser[a], b)
def _check_nonfatal(self, ex):
return ex.code < 400 or ex.code in [404, 429]
@@ -72,6 +80,7 @@ class HttpCli(object):
"""returns true if connection can be reused"""
self.keepalive = False
self.headers = {}
self.hint = None
try:
headerlines = read_header(self.sr)
if not headerlines:
@@ -104,10 +113,21 @@ class HttpCli(object):
v = self.headers.get("connection", "").lower()
self.keepalive = not v.startswith("close") and self.http_ver != "HTTP/1.0"
v = self.headers.get("x-forwarded-for", None)
if v is not None and self.conn.addr[0] in ["127.0.0.1", "::1"]:
self.ip = v.split(",")[0]
self.log_src = self.conn.set_rproxy(self.ip)
n = self.args.rproxy
if n:
v = self.headers.get("x-forwarded-for")
if v and self.conn.addr[0] in ["127.0.0.1", "::1"]:
if n > 0:
n -= 1
vs = v.split(",")
try:
self.ip = vs[n].strip()
except:
self.ip = vs[0].strip()
self.log("rproxy={} oob x-fwd {}".format(self.args.rproxy, v), c=3)
self.log_src = self.conn.set_rproxy(self.ip)
if self.args.ihead:
keys = self.args.ihead
@@ -119,6 +139,9 @@ class HttpCli(object):
if v is not None:
self.log("[H] {}: \033[33m[{}]".format(k, v), 6)
if "&" in self.req and "?" not in self.req:
self.hint = "did you mean '?' instead of '&'"
# split req into vpath + uparam
uparam = {}
if "?" not in self.req:
@@ -188,6 +211,9 @@ class HttpCli(object):
self.log("{}\033[0m, {}".format(str(ex), 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)
return self.keepalive
except Pebkac:
@@ -245,10 +271,11 @@ class HttpCli(object):
if self.is_rclone:
return ""
cmap = {"pw": "cppwd"}
kv = {
k: v
for k, v in self.uparam.items()
if k not in rm and self.cookies.get(k) != v
if k not in rm and self.cookies.get(cmap.get(k, k)) != v
}
kv.update(add)
if not kv:
@@ -569,11 +596,20 @@ class HttpCli(object):
if sub:
try:
dst = os.path.join(vfs.realpath, rem)
os.makedirs(fsenc(dst))
except:
if not os.path.isdir(fsenc(dst)):
os.makedirs(fsenc(dst))
except OSError as ex:
self.log("makedirs failed [{}]".format(dst))
if ex.errno == 13:
raise Pebkac(500, "the server OS denied write-access")
if ex.errno == 17:
raise Pebkac(400, "some file got your folder name")
raise Pebkac(500, min_ex())
except:
raise Pebkac(500, min_ex())
x = self.conn.hsrv.broker.put(True, "up2k.handle_json", body)
ret = x.get()
if sub:
@@ -602,7 +638,7 @@ class HttpCli(object):
penalty = 0.7
t_idle = t0 - idx.p_end
if idx.p_dur > 0.7 and t_idle < penalty:
m = "rate-limit ({:.1f} sec), cost {:.2f}, idle {:.2f}"
m = "rate-limit {:.1f} sec, cost {:.2f}, idle {:.2f}"
raise Pebkac(429, m.format(penalty, idx.p_dur, t_idle))
if "srch" in body:
@@ -758,8 +794,13 @@ class HttpCli(object):
try:
os.mkdir(fsenc(fn))
except OSError as ex:
if ex.errno == 13:
raise Pebkac(500, "the server OS denied write-access")
raise Pebkac(500, "mkdir failed:\n" + min_ex())
except:
raise Pebkac(500, "mkdir failed, check the logs")
raise Pebkac(500, min_ex())
vpath = "{}/{}".format(self.vpath, sanitized).lstrip("/")
self.redirect(vpath)
@@ -1176,7 +1217,7 @@ class HttpCli(object):
#
# send reply
if not is_compressed:
if not is_compressed and "cache" not in self.uparam:
self.out_headers.update(NO_CACHE)
self.out_headers["Accept-Ranges"] = "bytes"
@@ -1279,7 +1320,7 @@ class HttpCli(object):
ext = "folder"
exact = True
bad = re.compile(r"[](){}/[]|^[0-9_-]*$")
bad = re.compile(r"[](){}/ []|^[0-9_-]*$")
n = ext.split(".")[::-1]
if not exact:
n = n[:-1]
@@ -1423,33 +1464,8 @@ class HttpCli(object):
if self.args.no_stack:
raise Pebkac(403, "disabled by argv")
threads = {}
names = dict([(t.ident, t.name) for t in threading.enumerate()])
for tid, stack in sys._current_frames().items():
name = "{} ({:x})".format(names.get(tid), tid)
threads[name] = stack
rret = []
bret = []
for name, stack in sorted(threads.items()):
ret = ["\n\n# {}".format(name)]
pad = None
for fn, lno, name, line in traceback.extract_stack(stack):
fn = os.sep.join(fn.split(os.sep)[-3:])
ret.append('File: "{}", line {}, in {}'.format(fn, lno, name))
if line:
ret.append(" " + str(line.strip()))
if "self.not_empty.wait()" in line:
pad = " " * 4
if pad:
bret += [ret[0]] + [pad + x for x in ret[1:]]
else:
rret += ret
ret = rret + bret
ret = ("<pre>" + "\n".join(ret)).encode("utf-8")
self.reply(ret)
ret = "<pre>{}\n{}".format(time.time(), alltrace())
self.reply(ret.encode("utf-8"))
def tx_tree(self):
top = self.uparam["tree"] or ""
@@ -1764,28 +1780,44 @@ class HttpCli(object):
fn = f["name"]
rd = f["rd"]
del f["rd"]
if icur:
if vn != dbv:
_, rd = vn.get_dbv(rd)
if not icur:
break
if vn != dbv:
_, rd = vn.get_dbv(rd)
q = "select w from up where rd = ? and fn = ?"
r = None
try:
r = icur.execute(q, (rd, fn)).fetchone()
except Exception as ex:
if "database is locked" in str(ex):
break
q = "select w from up where rd = ? and fn = ?"
try:
r = icur.execute(q, (rd, fn)).fetchone()
except:
args = s3enc(idx.mem_cur, rd, fn)
r = icur.execute(q, args).fetchone()
except:
m = "tag list error, {}/{}\n{}"
self.log(m.format(rd, fn, min_ex()))
break
tags = {}
f["tags"] = tags
tags = {}
f["tags"] = tags
if not r:
continue
if not r:
continue
w = r[0][:16]
q = "select k, v from mt where w = ? and k != 'x'"
w = r[0][:16]
q = "select k, v from mt where w = ? and k != 'x'"
try:
for k, v in icur.execute(q, (w,)):
taglist[k] = True
tags[k] = v
except:
m = "tag read error, {}/{} [{}]:\n{}"
self.log(m.format(rd, fn, w, min_ex()))
break
if icur:
taglist = [k for k in vn.flags.get("mte", "").split(",") if k in taglist]

View File

@@ -3,7 +3,6 @@ from __future__ import print_function, unicode_literals
import re
import os
import sys
import time
import socket
@@ -43,6 +42,7 @@ class HttpConn(object):
self.ico = Ico(self.args)
self.t0 = time.time()
self.stopping = False
self.nbyte = 0
self.workload = 0
self.u2idx = None
@@ -50,6 +50,14 @@ class HttpConn(object):
self.lf_url = re.compile(self.args.lf_url) if self.args.lf_url else None
self.set_rproxy()
def shutdown(self):
self.stopping = True
try:
self.s.shutdown(socket.SHUT_RDWR)
self.s.close()
except:
pass
def set_rproxy(self, ip=None):
if ip is None:
color = 36
@@ -163,7 +171,7 @@ class HttpConn(object):
self.log("client rejected our certificate (nice)")
elif "ALERT_CERTIFICATE_UNKNOWN" in em:
# chrome-android keeps doing this
# android-chrome keeps doing this
pass
else:
@@ -174,7 +182,7 @@ class HttpConn(object):
if not self.sr:
self.sr = Unrecv(self.s)
while True:
while not self.stopping:
if self.is_mp:
self.workload += 50
if self.workload >= 2 ** 31:

View File

@@ -80,7 +80,14 @@ class HttpSrv(object):
return len(self.clients)
def shutdown(self):
self.log("ok bye")
clients = list(self.clients.keys())
for cli in clients:
try:
cli.shutdown()
except:
pass
self.log("httpsrv-n", "ok bye")
def thr_client(self, sck, addr):
"""thread managing one tcp client"""
@@ -100,25 +107,35 @@ class HttpSrv(object):
thr.daemon = True
thr.start()
fno = sck.fileno()
try:
if self.args.log_conn:
self.log("%s %s" % addr, "|%sC-crun" % ("-" * 6,), c="1;30")
cli.run()
except (OSError, socket.error) as ex:
if ex.errno not in [10038, 10054, 107, 57, 9]:
self.log(
"%s %s" % addr,
"run({}): {}".format(fno, ex),
c=6,
)
finally:
sck = cli.s
if self.args.log_conn:
self.log("%s %s" % addr, "|%sC-cdone" % ("-" * 7,), c="1;30")
try:
fno = sck.fileno()
sck.shutdown(socket.SHUT_RDWR)
sck.close()
except (OSError, socket.error) as ex:
if not MACOS:
self.log(
"%s %s" % addr,
"shut({}): {}".format(sck.fileno(), ex),
"shut({}): {}".format(fno, ex),
c="1;30",
)
if ex.errno not in [10038, 10054, 107, 57, 9]:

View File

@@ -8,7 +8,7 @@ import shutil
import subprocess as sp
from .__init__ import PY2, WINDOWS
from .util import fsenc, fsdec, REKOBO_LKEY
from .util import fsenc, fsdec, uncyg, REKOBO_LKEY
if not PY2:
unicode = str
@@ -16,6 +16,7 @@ if not PY2:
def have_ff(cmd):
if PY2:
print("# checking {}".format(cmd))
cmd = (cmd + " -version").encode("ascii").split(b" ")
try:
sp.Popen(cmd, stdout=sp.PIPE, stderr=sp.PIPE).communicate()
@@ -43,6 +44,9 @@ class MParser(object):
while True:
try:
bp = os.path.expanduser(args)
if WINDOWS:
bp = uncyg(bp)
if os.path.exists(bp):
self.bin = bp
return
@@ -111,6 +115,19 @@ def parse_ffprobe(txt):
ret = {} # processed
md = {} # raw tags
is_audio = fmt.get("format_name") in ["mp3", "ogg", "flac", "wav"]
if fmt.get("filename", "").split(".")[-1].lower() in ["m4a", "aac"]:
is_audio = True
# if audio file, ensure audio stream appears first
if (
is_audio
and len(streams) > 2
and streams[1].get("codec_type") != "audio"
and streams[2].get("codec_type") == "audio"
):
streams = [fmt, streams[2], streams[1]] + streams[3:]
have = {}
for strm in streams:
typ = strm.get("codec_type")
@@ -130,9 +147,7 @@ def parse_ffprobe(txt):
]
if typ == "video":
if strm.get("DISPOSITION:attached_pic") == "1" or fmt.get(
"format_name"
) in ["mp3", "ogg", "flac"]:
if strm.get("DISPOSITION:attached_pic") == "1" or is_audio:
continue
kvm = [
@@ -176,7 +191,7 @@ def parse_ffprobe(txt):
k = k[4:].strip()
v = v.strip()
if k and v:
if k and v and k not in md:
md[k] = [v]
for k in [".q", ".vq", ".aq"]:

View File

@@ -21,6 +21,7 @@ class TcpSrv(object):
self.log = hub.log
self.num_clients = Counter()
self.stopping = False
ip = "127.0.0.1"
eps = {ip: "local only"}
@@ -67,7 +68,7 @@ class TcpSrv(object):
ip, port = srv.getsockname()
self.log("tcpsrv", "listening @ {0}:{1}".format(ip, port))
while True:
while not self.stopping:
if self.args.log_conn:
self.log("tcpsrv", "|%sC-ncli" % ("-" * 1,), c="1;30")
@@ -78,8 +79,18 @@ class TcpSrv(object):
if self.args.log_conn:
self.log("tcpsrv", "|%sC-acc1" % ("-" * 2,), c="1;30")
ready, _, _ = select.select(self.srv, [], [])
try:
# macos throws bad-fd
ready, _, _ = select.select(self.srv, [], [])
except:
ready = []
if not self.stopping:
raise
for srv in ready:
if self.stopping:
break
sck, addr = srv.accept()
sip, sport = srv.getsockname()
if self.args.log_conn:
@@ -95,6 +106,13 @@ class TcpSrv(object):
self.hub.broker.put(False, "httpconn", sck, addr)
def shutdown(self):
self.stopping = True
try:
for srv in self.srv:
srv.close()
except:
pass
self.log("tcpsrv", "ok bye")
def detect_interfaces(self, listen_ips):

View File

@@ -84,14 +84,14 @@ def thumb_path(histpath, rem, mtime, fmt):
fn = rem
if rd:
h = hashlib.sha512(fsenc(rd)).digest()[:24]
h = hashlib.sha512(fsenc(rd)).digest()
b64 = base64.urlsafe_b64encode(h).decode("ascii")[:24]
rd = "{}/{}/".format(b64[:2], b64[2:4]).lower() + b64
else:
rd = "top"
# could keep original filenames but this is safer re pathlen
h = hashlib.sha512(fsenc(fn)).digest()[:24]
h = hashlib.sha512(fsenc(fn)).digest()
fn = base64.urlsafe_b64encode(h).decode("ascii")[:24]
return "{}/th/{}/{}.{:x}.{}".format(
@@ -154,7 +154,8 @@ class ThumbSrv(object):
histpath = self.asrv.vfs.histtab[ptop]
tpath = thumb_path(histpath, rem, mtime, fmt)
abspath = os.path.join(ptop, rem)
cond = threading.Condition()
cond = threading.Condition(self.mutex)
do_conv = False
with self.mutex:
try:
self.busy[tpath].append(cond)
@@ -172,8 +173,11 @@ class ThumbSrv(object):
f.write(fsenc(os.path.dirname(abspath)))
self.busy[tpath] = [cond]
self.q.put([abspath, tpath])
self.log("conv {} \033[0m{}".format(tpath, abspath), c=6)
do_conv = True
if do_conv:
self.q.put([abspath, tpath])
self.log("conv {} \033[0m{}".format(tpath, abspath), c=6)
while not self.stopping:
with self.mutex:
@@ -181,7 +185,7 @@ class ThumbSrv(object):
break
with cond:
cond.wait()
cond.wait(3)
try:
st = os.stat(tpath)
@@ -335,29 +339,32 @@ class ThumbSrv(object):
interval = self.args.th_clean
while True:
time.sleep(interval)
ndirs = 0
for vol, histpath in self.asrv.vfs.histtab.items():
if histpath.startswith(vol):
self.log("\033[Jcln {}/\033[A".format(histpath))
else:
self.log("\033[Jcln {} ({})/\033[A".format(histpath, vol))
self.clean(histpath)
ndirs += self.clean(histpath)
self.log("\033[Jcln ok")
self.log("\033[Jcln ok; rm {} dirs".format(ndirs))
def clean(self, histpath):
# self.log("cln {}".format(histpath))
thumbpath = os.path.join(histpath, "th")
# self.log("cln {}".format(thumbpath))
maxage = self.args.th_maxage
now = time.time()
prev_b64 = None
prev_fp = None
try:
ents = os.listdir(histpath)
ents = os.listdir(thumbpath)
except:
return
return 0
ndirs = 0
for f in sorted(ents):
fp = os.path.join(histpath, f)
fp = os.path.join(thumbpath, f)
cmp = fp.lower().replace("\\", "/")
# "top" or b64 prefix/full (a folder)
@@ -372,10 +379,11 @@ class ThumbSrv(object):
break
if safe:
ndirs += 1
self.log("rm -rf [{}]".format(fp))
shutil.rmtree(fp, ignore_errors=True)
else:
self.clean(fp)
ndirs += self.clean(fp)
continue
# thumb file
@@ -397,3 +405,5 @@ class ThumbSrv(object):
prev_b64 = b64
prev_fp = fp
return ndirs

View File

@@ -26,7 +26,7 @@ class U2idx(object):
self.timeout = self.args.srch_time
if not HAVE_SQLITE3:
self.log("could not load sqlite3; searchign wqill be disabled")
self.log("your python does not have sqlite3; searching will be disabled")
return
self.cur = {}
@@ -57,6 +57,9 @@ class U2idx(object):
raise Pebkac(500, min_ex())
def get_cur(self, ptop):
if not HAVE_SQLITE3:
return None
cur = self.cur.get(ptop)
if cur:
return cur
@@ -66,7 +69,7 @@ class U2idx(object):
if not os.path.exists(db_path):
return None
cur = sqlite3.connect(db_path).cursor()
cur = sqlite3.connect(db_path, 2).cursor()
self.cur[ptop] = cur
return cur

View File

@@ -16,7 +16,7 @@ import traceback
import subprocess as sp
from copy import deepcopy
from .__init__ import WINDOWS, ANYWIN
from .__init__ import WINDOWS, ANYWIN, PY2
from .util import (
Pebkac,
Queue,
@@ -30,6 +30,7 @@ from .util import (
s3dec,
statdir,
s2hms,
min_ex,
)
from .mtag import MTag, MParser
@@ -39,6 +40,8 @@ try:
except:
HAVE_SQLITE3 = False
DB_VER = 4
class Up2k(object):
"""
@@ -91,7 +94,7 @@ class Up2k(object):
thr.start()
# static
self.r_hash = re.compile("^[0-9a-zA-Z_-]{43}$")
self.r_hash = re.compile("^[0-9a-zA-Z_-]{44}$")
if not HAVE_SQLITE3:
self.log("could not initialize sqlite3, will use in-memory registry only")
@@ -134,7 +137,7 @@ class Up2k(object):
def get_state(self):
mtpq = 0
q = "select count(w) from mt where k = 't:mtp'"
got_lock = self.mutex.acquire(timeout=0.5)
got_lock = False if PY2 else self.mutex.acquire(timeout=0.5)
if got_lock:
for cur in self.cur.values():
try:
@@ -422,7 +425,10 @@ class Up2k(object):
ret += self._build_dir(dbw, top, excl, abspath, nohash)
else:
# self.log("file: {}".format(abspath))
rp = abspath[len(top) :].replace("\\", "/").strip("/")
rp = abspath[len(top) + 1 :]
if WINDOWS:
rp = rp.replace("\\", "/").strip("/")
rd, fn = rp.rsplit("/", 1) if "/" in rp else ["", rp]
sql = "select w, mt, sz from up where rd = ? and fn = ?"
try:
@@ -647,7 +653,7 @@ class Up2k(object):
try:
parser = MParser(parser)
except:
self.log("invalid argument: " + parser, 1)
self.log("invalid argument (could not find program): " + parser, 1)
return
for tag in entags:
@@ -887,59 +893,31 @@ class Up2k(object):
if not existed and ver is None:
return self._create_db(db_path, cur)
orig_ver = ver
if not ver or ver < 3:
bak = "{}.bak.{:x}.v{}".format(db_path, int(time.time()), ver)
db = cur.connection
cur.close()
db.close()
msg = "creating new DB (old is bad); backup: {}"
if ver:
msg = "creating backup before upgrade: {}"
self.log(msg.format(bak))
shutil.copy2(db_path, bak)
cur = self._orz(db_path)
if ver == 1:
cur = self._upgrade_v1(cur, db_path)
if cur:
ver = 2
if ver == 2:
cur = self._create_v3(cur)
ver = self._read_ver(cur) if cur else None
if ver == 3:
if orig_ver != ver:
cur.connection.commit()
cur.execute("vacuum")
cur.connection.commit()
if ver == DB_VER:
try:
nfiles = next(cur.execute("select count(w) from up"))[0]
self.log("OK: {} |{}|".format(db_path, nfiles))
return cur
except Exception as ex:
self.log("WARN: could not list files, DB corrupt?\n " + repr(ex))
except:
self.log("WARN: could not list files; DB corrupt?\n" + min_ex())
if cur:
db = cur.connection
cur.close()
db.close()
if (ver or 0) > DB_VER:
m = "database is version {}, this copyparty only supports versions <= {}"
raise Exception(m.format(ver, DB_VER))
bak = "{}.bak.{:x}.v{}".format(db_path, int(time.time()), ver)
db = cur.connection
cur.close()
db.close()
msg = "creating new DB (old is bad); backup: {}"
if ver:
msg = "creating new DB (too old to upgrade); backup: {}"
self.log(msg.format(bak))
os.rename(fsenc(db_path), fsenc(bak))
return self._create_db(db_path, None)
def _create_db(self, db_path, cur):
if not cur:
cur = self._orz(db_path)
self._create_v2(cur)
self._create_v3(cur)
cur.connection.commit()
self.log("created DB at {}".format(db_path))
return cur
def _read_ver(self, cur):
for tab in ["ki", "kv"]:
try:
@@ -951,65 +929,38 @@ class Up2k(object):
if rows:
return int(rows[0][0])
def _create_v2(self, cur):
for cmd in [
r"create table up (w text, mt int, sz int, rd text, fn text)",
r"create index up_rd on up(rd)",
r"create index up_fn on up(fn)",
]:
cur.execute(cmd)
return cur
def _create_v3(self, cur):
def _create_db(self, db_path, cur):
"""
collision in 2^(n/2) files where n = bits (6 bits/ch)
10*6/2 = 2^30 = 1'073'741'824, 24.1mb idx 1<<(3*10)
12*6/2 = 2^36 = 68'719'476'736, 24.8mb idx
16*6/2 = 2^48 = 281'474'976'710'656, 26.1mb idx
"""
for c, ks in [["drop table k", "isv"], ["drop index up_", "w"]]:
for k in ks:
try:
cur.execute(c + k)
except:
pass
if not cur:
cur = self._orz(db_path)
idx = r"create index up_w on up(substr(w,1,16))"
if self.no_expr_idx:
idx = r"create index up_w on up(w)"
for cmd in [
r"create table up (w text, mt int, sz int, rd text, fn text)",
r"create index up_rd on up(rd)",
r"create index up_fn on up(fn)",
idx,
r"create table mt (w text, k text, v int)",
r"create index mt_w on mt(w)",
r"create index mt_k on mt(k)",
r"create index mt_v on mt(v)",
r"create table kv (k text, v int)",
r"insert into kv values ('sver', 3)",
r"insert into kv values ('sver', {})".format(DB_VER),
]:
cur.execute(cmd)
cur.connection.commit()
self.log("created DB at {}".format(db_path))
return cur
def _upgrade_v1(self, odb, db_path):
npath = db_path + ".next"
if os.path.exists(npath):
os.unlink(npath)
ndb = self._orz(npath)
self._create_v2(ndb)
c = odb.execute("select * from up")
for wark, ts, sz, rp in c:
rd, fn = rp.rsplit("/", 1) if "/" in rp else ["", rp]
v = (wark, ts, sz, rd, fn)
ndb.execute("insert into up values (?,?,?,?,?)", v)
ndb.connection.commit()
ndb.connection.close()
odb.connection.close()
atomic_move(npath, db_path)
return self._orz(db_path)
def handle_json(self, cj):
with self.mutex:
if not self.register_vpath(cj["ptop"], cj["vcfg"]):
@@ -1022,7 +973,7 @@ class Up2k(object):
now = time.time()
job = None
with self.mutex:
cur = self.cur.get(cj["ptop"], None)
cur = self.cur.get(cj["ptop"])
reg = self.registry[cj["ptop"]]
if cur:
if self.no_expr_idx:
@@ -1180,7 +1131,7 @@ class Up2k(object):
def handle_chunk(self, ptop, wark, chash):
with self.mutex:
job = self.registry[ptop].get(wark, None)
job = self.registry[ptop].get(wark)
if not job:
known = " ".join([x for x in self.registry[ptop].keys()])
self.log("unknown wark [{}], known: {}".format(wark, known))
@@ -1245,7 +1196,7 @@ class Up2k(object):
return ret, dst
def idx_wark(self, ptop, wark, rd, fn, lmod, sz):
cur = self.cur.get(ptop, None)
cur = self.cur.get(ptop)
if not cur:
return False
@@ -1316,9 +1267,9 @@ class Up2k(object):
hashobj.update(buf)
rem -= len(buf)
digest = hashobj.digest()[:32]
digest = hashobj.digest()[:33]
digest = base64.urlsafe_b64encode(digest)
ret.append(digest.decode("utf-8").rstrip("="))
ret.append(digest.decode("utf-8"))
return ret
@@ -1414,7 +1365,7 @@ class Up2k(object):
newest = max(x["poke"] for _, x in reg.items()) if reg else 0
etag = [len(reg), newest]
if etag == prev.get(ptop, None):
if etag == prev.get(ptop):
return
try:
@@ -1518,12 +1469,12 @@ def up2k_wark_from_hashlist(salt, filesize, hashes):
ident.extend(hashes)
ident = "\n".join(ident)
wark = hashlib.sha512(ident.encode("utf-8")).digest()
wark = hashlib.sha512(ident.encode("utf-8")).digest()[:33]
wark = base64.urlsafe_b64encode(wark)
return wark.decode("ascii")[:43]
return wark.decode("ascii")
def up2k_wark_from_metadata(salt, sz, lastmod, rd, fn):
ret = fsenc("{}\n{}\n{}\n{}\n{}".format(salt, lastmod, sz, rd, fn))
ret = base64.urlsafe_b64encode(hashlib.sha512(ret).digest())
return "#{}".format(ret[:42].decode("ascii"))
return "#{}".format(ret.decode("ascii"))[:44]

View File

@@ -254,6 +254,34 @@ def trace(*args, **kwargs):
nuprint(msg)
def alltrace():
threads = {}
names = dict([(t.ident, t.name) for t in threading.enumerate()])
for tid, stack in sys._current_frames().items():
name = "{} ({:x})".format(names.get(tid), tid)
threads[name] = stack
rret = []
bret = []
for name, stack in sorted(threads.items()):
ret = ["\n\n# {}".format(name)]
pad = None
for fn, lno, name, line in traceback.extract_stack(stack):
fn = os.sep.join(fn.split(os.sep)[-3:])
ret.append('File: "{}", line {}, in {}'.format(fn, lno, name))
if line:
ret.append(" " + str(line.strip()))
if "self.not_empty.wait()" in line:
pad = " " * 4
if pad:
bret += [ret[0]] + [pad + x for x in ret[1:]]
else:
rret += ret
return "\n".join(rret + bret)
def min_ex():
et, ev, tb = sys.exc_info()
tb = traceback.extract_tb(tb, 2)
@@ -323,7 +351,7 @@ def ren_open(fname, *args, **kwargs):
if not b64:
b64 = (bname + ext).encode("utf-8", "replace")
b64 = hashlib.sha512(b64).digest()[:12]
b64 = base64.urlsafe_b64encode(b64).decode("utf-8").rstrip("=")
b64 = base64.urlsafe_b64encode(b64).decode("utf-8")
badlen = len(fname)
while len(fname) >= badlen:
@@ -620,6 +648,16 @@ def s2hms(s, optional_h=False):
return "{}:{:02}:{:02}".format(h, m, s)
def uncyg(path):
if len(path) < 2 or not path.startswith("/"):
return path
if len(path) > 2 and path[2] != "/":
return path
return "{}:\\{}".format(path[1], path[3:])
def undot(path):
ret = []
for node in path.split("/"):
@@ -880,8 +918,8 @@ def hashcopy(actor, fin, fout):
hashobj.update(buf)
fout.write(buf)
digest32 = hashobj.digest()[:32]
digest_b64 = base64.urlsafe_b64encode(digest32).decode("utf-8").rstrip("=")
digest = hashobj.digest()[:33]
digest_b64 = base64.urlsafe_b64encode(digest).decode("utf-8")
return tlen, hashobj.hexdigest(), digest_b64
@@ -992,7 +1030,13 @@ def guess_mime(url, fallback="application/octet-stream"):
except:
return fallback
return MIMES.get(ext) or mimetypes.guess_type(url)[0] or fallback
ret = MIMES.get(ext) or mimetypes.guess_type(url)[0] or fallback
if ";" not in ret:
if ret.startswith("text/") or ret.endswith("/javascript"):
ret += "; charset=UTF-8"
return ret
def runcmd(*argv):

View File

@@ -25,6 +25,35 @@ html, body {
body {
padding-bottom: 5em;
}
#tt {
position: fixed;
max-width: 34em;
background: #222;
border: 0 solid #555;
overflow: hidden;
margin-top: 1em;
padding: 0 1em;
height: 0;
opacity: .1;
transition: opacity 0.14s, height 0.14s, padding 0.14s;
box-shadow: 0 .2em .5em #222;
border-radius: .4em;
z-index: 9001;
}
#tt.show {
padding: 1em;
height: auto;
border-width: .2em 0;
opacity: 1;
}
#tt code {
background: #3c3c3c;
padding: .2em .3em;
border-top: 1px solid #777;
border-radius: .3em;
font-family: monospace, monospace;
line-height: 2em;
}
#path,
#path * {
font-size: 1em;
@@ -53,6 +82,7 @@ body {
#files tbody a {
display: block;
padding: .3em 0;
scroll-margin-top: 45vh;
}
#files tbody div a {
color: #f5a;
@@ -68,7 +98,6 @@ a, #files tbody div a:last-child {
text-decoration: underline;
}
#files thead {
background: #333;
position: sticky;
top: 0;
}
@@ -76,29 +105,30 @@ a, #files tbody div a:last-child {
color: #999;
font-weight: normal;
}
#files tr:hover {
#files tr:hover td {
background: #1c1c1c;
}
#files thead th {
padding: .5em 1.3em .3em 1.3em;
padding: .5em .3em .3em .3em;
border-right: 2px solid #3c3c3c;
border-bottom: 2px solid #444;
background: #333;
cursor: pointer;
}
#files thead th+th {
border-left: 2px solid #2a2a2a;
}
#files thead th:last-child {
background: #444;
border-radius: .7em .7em 0 0;
border-right: none;
}
#files thead th:first-child {
#files tbody {
background: #222;
}
#files tbody,
#files thead th:nth-child(2) {
background: #222;
border-radius: 0 .7em 0 0;
}
#files td {
margin: 0;
padding: 0 .5em;
border-bottom: 1px solid #111;
border-left: 1px solid #2c2c2c;
}
#files td+td+td {
max-width: 30em;
@@ -185,9 +215,17 @@ a, #files tbody div a:last-child {
margin: -.2em;
}
#files tbody a.play.act {
color: #840;
color: #720;
text-shadow: 0 0 .3em #b80;
}
#ggrid a.play,
html.light #ggrid a.play {
color: #fff;
background: #750;
border-color: #c90;
border-top: 1px solid #da4;
box-shadow: 0 .1em 1.2em #b83;
}
#files tbody tr.sel td,
#ggrid a.sel,
html.light #ggrid a.sel {
@@ -209,11 +247,17 @@ html.light #ggrid a.sel {
box-shadow: 0 .1em 1.2em #b36;
transition: all 0.2s cubic-bezier(.2, 2.2, .5, 1); /* https://cubic-bezier.com/#.4,2,.7,1 */
}
#ggrid a.sel img {
#ggrid a.sel img,
#ggrid a.play img {
opacity: .7;
box-shadow: 0 0 1em #b36;
filter: contrast(130%) brightness(107%);
}
#ggrid a.sel img {
box-shadow: 0 0 1em #b36;
}
#ggrid a.play img {
box-shadow: 0 0 1em #b83;
}
#files tr.sel a {
color: #fff;
}
@@ -267,6 +311,7 @@ html.light #ggrid a.sel {
height: 6em;
width: 100%;
z-index: 3;
touch-action: none;
transition: bottom 0.15s;
}
#widget.open {
@@ -483,20 +528,56 @@ html.light #ggrid a.sel {
margin: .5em;
}
.opview input[type=text] {
color: #fff;
background: #383838;
color: #fff;
border: none;
box-shadow: 0 0 .3em #222;
border-bottom: 1px solid #fc5;
border-radius: .2em;
padding: .2em .3em;
}
.opview input.err,
html.light .opview input[type="text"].err {
color: #fff;
background: #a20;
border-color: #f00;
box-shadow: 0 0 .7em #f00;
text-shadow: 1px 1px 0 #500;
outline: none;
}
input[type="checkbox"]+label {
color: #f5a;
}
input[type="checkbox"]:checked+label {
color: #fc5;
}
input[type="radio"]:checked+label {
color: #fc0;
}
html.light input[type="radio"]:checked+label {
color: #07c;
}
input.eq_gain {
width: 3em;
text-align: center;
margin: 0 .6em;
}
#audio_eq table {
border-collapse: collapse;
}
#audio_eq td {
text-align: center;
}
#audio_eq a.eq_step {
font-size: 1.5em;
display: block;
padding: 0;
}
#au_eq {
display: block;
margin-top: .5em;
padding: 1.3em .3em;
}
@@ -563,6 +644,7 @@ input[type="checkbox"]:checked+label {
}
#wrap {
margin-top: 2em;
min-height: 90vh;
}
#tree {
display: none;
@@ -575,8 +657,15 @@ input[type="checkbox"]:checked+label {
overscroll-behavior-y: none;
scrollbar-color: #eb0 #333;
}
#treeh {
background: #333;
position: sticky;
z-index: 1;
top: 0;
}
#thx_ff {
padding: 5em 0;
/* widget */
}
#tree::-webkit-scrollbar-track,
#tree::-webkit-scrollbar {
@@ -600,6 +689,7 @@ input[type="checkbox"]:checked+label {
box-shadow: 0 .1em .2em #222 inset;
border-radius: .3em;
margin: .2em;
white-space: pre;
position: relative;
top: -.2em;
}
@@ -636,10 +726,10 @@ input[type="checkbox"]:checked+label {
#treeul a.hl {
color: #400;
background: #fc4;
border-radius: .3em;
text-shadow: none;
}
#treeul a {
border-radius: .3em;
display: inline-block;
}
#treeul a+a {
@@ -667,34 +757,20 @@ input[type="checkbox"]:checked+label {
font-size: 2em;
white-space: nowrap;
}
#files th:hover .cfg,
#files th.min .cfg {
#files th:hover .cfg {
display: block;
width: 1em;
border-radius: .2em;
margin: -1.3em auto 0 auto;
background: #444;
}
#files th.min .cfg {
margin: -.6em;
}
#files>thead>tr>th.min span {
position: absolute;
transform: rotate(270deg);
background: linear-gradient(90deg, rgba(68,68,68,0), rgba(68,68,68,0.5) 70%, #444);
margin-left: -4.6em;
padding: .4em;
top: 5.4em;
width: 8em;
text-align: right;
letter-spacing: .04em;
#files>thead>tr>th.min,
#files td.min {
display: none;
}
#files td:nth-child(2n) {
color: #f5a;
}
#files td.min a {
display: none;
}
#files tr.play td,
#files tr.play div a {
background: #fc4;
@@ -709,51 +785,38 @@ input[type="checkbox"]:checked+label {
color: #300;
background: #fea;
}
#op_cfg {
.opwide {
max-width: none;
margin-right: 1.5em;
}
#op_cfg>div>a {
.opwide>div {
display: inline-block;
vertical-align: top;
border-left: .2em solid #4c4c4c;
margin-left: .5em;
padding-left: .5em;
}
.opwide>div.fill {
display: block;
}
.opwide>div>div>a {
line-height: 2em;
}
#op_cfg>div>span {
#op_cfg>div>div>span {
display: inline-block;
padding: .2em .4em;
}
#op_cfg h3 {
.opbox h3 {
margin: .8em 0 0 .6em;
padding: 0;
border-bottom: 1px solid #555;
}
#opdesc {
display: none;
}
#ops:hover #opdesc {
display: block;
background: linear-gradient(0deg,#555, #4c4c4c 80%, #444);
box-shadow: 0 .3em 1em #222;
padding: 1em;
border-radius: .3em;
position: absolute;
z-index: 3;
top: 6em;
right: 1.5em;
}
#ops:hover #opdesc.off {
display: none;
}
#opdesc code {
background: #3c3c3c;
padding: .2em .3em;
border-top: 1px solid #777;
border-radius: .3em;
font-family: monospace, monospace;
line-height: 2em;
}
#thumbs {
#thumbs,
#au_osd_cv {
opacity: .3;
}
#griden.on+#thumbs {
#griden.on+#thumbs,
#au_os_ctl.on+#au_osd_cv {
opacity: 1;
}
#ghead {
@@ -804,7 +867,6 @@ html.light #ghead {
content: '📂';
line-height: 0;
font-size: 2em;
display: inline-block;
margin: -.7em .1em -.5em -.3em;
}
#ggrid a:hover {
@@ -857,6 +919,15 @@ html.light {
background: #eee;
text-shadow: none;
}
html.light #tt {
background: #fff;
border-color: #888;
box-shadow: 0 .3em 1em rgba(0,0,0,0.4);
}
html.light #tt code {
background: #060;
color: #fff;
}
html.light #ops,
html.light .opbox,
html.light #srch_form {
@@ -900,8 +971,14 @@ html.light #treeul a.hl {
background: #07a;
color: #fff;
}
html.light #treeul a.hl:hover {
background: #059;
}
html.light #tree li {
border-color: #ddd #fff #f7f7f7 #fff;
border-color: #f7f7f7 #fff #ddd #fff;
}
html.light #tree a:hover {
background: #fff;
}
html.light #tree ul {
border-color: #ccc;
@@ -919,13 +996,14 @@ html.light #files {
}
html.light #files thead th {
background: #eee;
border-radius: 0;
border: 1px solid #ccc;
border-top: none;
}
html.light #files tr td {
border-top: 1px solid #ddd;
html.light #files thead th+th {
border-left: 1px solid #f7f7f7;
}
html.light #files td {
border-bottom: 1px solid #f7f7f7;
border-color: #fff #fff #ddd #ddd;
}
html.light #files tbody tr:last-child td {
border-bottom: .2em solid #ccc;
@@ -933,25 +1011,28 @@ html.light #files tbody tr:last-child td {
html.light #files td:nth-child(2n) {
color: #d38;
}
html.light #files tr:hover td {
background: #fff;
html.light #files tr.play td:nth-child(2n) {
color: #c16;
}
html.light #files tbody a.play {
color: #c0f;
}
html.light tr.play td {
html.light #files tbody a.play.act {
color: #90c;
}
html.light #files tr.play td {
background: #fc5;
border-color: #eb1;
}
html.light #files tr:hover td {
background: #fff;
}
html.light tr.play a {
color: #406;
}
html.light #files th:hover .cfg,
html.light #files th.min .cfg {
html.light #files th:hover .cfg {
background: #ccc;
}
html.light #files > thead > tr > th.min span {
background: linear-gradient(90deg, rgba(204,204,204,0), rgba(204,204,204,0.5) 70%, #ccc);
}
html.light #blocked {
background: #eee;
}
@@ -961,7 +1042,24 @@ html.light #blk_abrt a {
box-shadow: 0 .2em .4em #ddd;
}
html.light #widget a {
color: #fc5;
color: #06a;
}
html.light #wtoggle,
html.light #widgeti {
background: #eee;
}
html.light #wtoggle {
box-shadow: 0 0 .5em #bbb;
}
html.light #widget.open {
border-top: .2em solid #f7f7f7;
}
html.light #wzip,
html.light #wnp {
border-color: #ccc;
}
html.light #barbuf {
background: none;
}
html.light #files tr.sel:hover td {
background: #c37;
@@ -978,20 +1076,15 @@ html.light #files tr.sel a.play.act {
html.light input[type="checkbox"] + label {
color: #333;
}
html.light .opwide>div {
border-color: #ccc;
}
html.light .opview input[type="text"] {
background: #fff;
color: #333;
box-shadow: 0 0 2px #888;
border-color: #38d;
}
html.light #ops:hover #opdesc {
background: #fff;
box-shadow: 0 .3em 1em #ccc;
}
html.light #opdesc code {
background: #060;
color: #fff;
}
html.light #u2tab a>span,
html.light #files td div span {
color: #000;
@@ -1001,9 +1094,6 @@ html.light #path {
text-shadow: none;
box-shadow: 0 0 .3em #bbb;
}
html.light #path a {
color: #333;
}
html.light #path a:not(:last-child)::after {
border-color: #ccc;
background: none;
@@ -1012,7 +1102,7 @@ html.light #path a:not(:last-child)::after {
}
html.light #path a:hover {
background: none;
color: #60a;
color: #90d;
}
html.light #files tbody div a {
color: #d38;
@@ -1022,6 +1112,9 @@ html.light #files tr.sel a:hover {
color: #000;
background: #fff;
}
html.light #treeh {
background: #eee;
}
html.light #tree {
scrollbar-color: #a70 #ddd;
}
@@ -1040,6 +1133,7 @@ html.light #tree::-webkit-scrollbar {
opacity: 0;
position: fixed;
overflow: hidden;
touch-action: none;
top: 0;
left: 0;
width: 100%;

View File

@@ -2,134 +2,134 @@
<html lang="en">
<head>
<meta charset="utf-8">
<title>⇆🎉 {{ title }}</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=0.8">
<link rel="stylesheet" type="text/css" media="screen" href="/.cpr/browser.css{{ ts }}">
<link rel="stylesheet" type="text/css" media="screen" href="/.cpr/upload.css{{ ts }}">
{%- if css %}
<link rel="stylesheet" type="text/css" media="screen" href="{{ css }}{{ ts }}">
{%- endif %}
<meta charset="utf-8">
<title>⇆🎉 {{ title }}</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=0.8">
<link rel="stylesheet" type="text/css" media="screen" href="/.cpr/browser.css{{ ts }}">
<link rel="stylesheet" type="text/css" media="screen" href="/.cpr/upload.css{{ ts }}">
{%- if css %}
<link rel="stylesheet" type="text/css" media="screen" href="{{ css }}{{ ts }}">
{%- endif %}
</head>
<body>
<div id="ops">
<a href="#" data-dest="" data-desc="close submenu">---</a>
{%- if have_up2k_idx %}
<a href="#" data-perm="read" data-dest="search" data-desc="search for files by attributes, path/name, music tags, or any combination of those.&lt;br /&gt;&lt;br /&gt;&lt;code&gt;foo bar&lt;/code&gt; = must contain both foo and bar,&lt;br /&gt;&lt;code&gt;foo -bar&lt;/code&gt; = must contain foo but not bar,&lt;br /&gt;&lt;code&gt;^yana .opus$&lt;/code&gt; = must start with yana and have the opus extension">🔎</a>
<a href="#" data-dest="up2k" data-desc="up2k: upload files (if you have write-access) or toggle into the search-mode and drag files onto the search button to see if they exist somewhere on the server">🚀</a>
{%- else %}
<a href="#" data-perm="write" data-dest="up2k" data-desc="up2k: upload files with resume support (close your browser and drop the same files in later)">🚀</a>
{%- endif %}
<a href="#" data-perm="write" data-dest="bup" data-desc="bup: basic uploader, even supports netscape 4.0">🎈</a>
<a href="#" data-perm="write" data-dest="mkdir" data-desc="mkdir: create a new directory">📂</a>
<a href="#" data-perm="read write" data-dest="new_md" data-desc="new-md: create a new markdown document">📝</a>
<a href="#" data-perm="write" data-dest="msg" data-desc="msg: send a message to the server log">📟</a>
<a href="#" data-dest="cfg" data-desc="configuration options">⚙️</a>
<div id="opdesc"></div>
</div>
<div id="ops"></div>
<div id="op_search" class="opview">
{%- if have_tags_idx %}
<div id="srch_form" class="tags"></div>
{%- else %}
<div id="srch_form"></div>
{%- endif %}
<div id="srch_q"></div>
</div>
<div id="op_search" class="opview">
{%- if have_tags_idx %}
<div id="srch_form" class="tags"></div>
{%- else %}
<div id="srch_form"></div>
{%- endif %}
<div id="srch_q"></div>
</div>
{%- include 'upload.html' %}
<div id="op_player" class="opview opbox opwide"></div>
<div id="op_cfg" class="opview opbox">
<h3>switches</h3>
<div>
<a id="tooltips" class="tgl btn" href="#">tooltips</a>
<a id="lightmode" class="tgl btn" href="#">lightmode</a>
<a id="griden" class="tgl btn" href="#">the grid</a>
<a id="thumbs" class="tgl btn" href="#">thumbs</a>
</div>
{%- if have_zip %}
<h3>folder download</h3>
<div id="arc_fmt"></div>
{%- endif %}
<h3>key notation</h3>
<div id="key_notation"></div>
</div>
<h1 id="path">
<a href="#" id="entree">🌲</a>
{%- for n in vpnodes %}
<a href="/{{ n[0] }}">{{ n[1] }}</a>
{%- endfor %}
</h1>
<div id="tree">
<a href="#" id="detree">🍞...</a>
<a href="#" class="btn" step="2" id="twobytwo">+</a>
<a href="#" class="btn" step="-2" id="twig">&ndash;</a>
<a href="#" class="tgl btn" id="dyntree">a</a>
<ul id="treeul"></ul>
<div id="thx_ff">&nbsp;</div>
</div>
<div id="op_bup" class="opview opbox act">
<div id="u2err"></div>
<form method="post" enctype="multipart/form-data" accept-charset="utf-8" action="{{ url_suf }}">
<input type="hidden" name="act" value="bput" />
<input type="file" name="f" multiple><br />
<input type="submit" value="start upload">
</form>
</div>
<div id="op_mkdir" class="opview opbox act">
<form method="post" enctype="multipart/form-data" accept-charset="utf-8" action="{{ url_suf }}">
<input type="hidden" name="act" value="mkdir" />
<input type="text" name="name" size="30">
<input type="submit" value="mkdir">
</form>
</div>
<div id="op_new_md" class="opview opbox">
<form method="post" enctype="multipart/form-data" accept-charset="utf-8" action="{{ url_suf }}">
<input type="hidden" name="act" value="new_md" />
<input type="text" name="name" size="30">
<input type="submit" value="create doc">
</form>
</div>
<div id="op_msg" class="opview opbox act">
<form method="post" enctype="application/x-www-form-urlencoded" accept-charset="utf-8" action="{{ url_suf }}">
<input type="text" name="msg" size="30">
<input type="submit" value="send msg">
</form>
</div>
<div id="op_up2k" class="opview"></div>
<div id="op_cfg" class="opview opbox opwide"></div>
<h1 id="path">
<a href="#" id="entree" tt="show directory tree$NHotkey: B">🌲</a>
{%- for n in vpnodes %}
<a href="/{{ n[0] }}">{{ n[1] }}</a>
{%- endfor %}
</h1>
<div id="tree"></div>
<div id="wrap">
<div id="pro" class="logue">{{ logues[0] }}</div>
<div id="pro" class="logue">{{ logues[0] }}</div>
<table id="files">
<thead>
<tr>
<th name="lead"><span>c</span></th>
<th name="href"><span>File Name</span></th>
<th name="sz" sort="int"><span>Size</span></th>
{%- for k in taglist %}
{%- if k.startswith('.') %}
<th name="tags/{{ k }}" sort="int"><span>{{ k[1:] }}</span></th>
{%- else %}
<th name="tags/{{ k }}"><span>{{ k[0]|upper }}{{ k[1:] }}</span></th>
{%- endif %}
{%- endfor %}
<th name="ext"><span>T</span></th>
<th name="ts"><span>Date</span></th>
</tr>
</thead>
<tbody>
<table id="files">
<thead>
<tr>
<th name="lead"><span>c</span></th>
<th name="href"><span>File Name</span></th>
<th name="sz" sort="int"><span>Size</span></th>
{%- for k in taglist %}
{%- if k.startswith('.') %}
<th name="tags/{{ k }}" sort="int"><span>{{ k[1:] }}</span></th>
{%- else %}
<th name="tags/{{ k }}"><span>{{ k[0]|upper }}{{ k[1:] }}</span></th>
{%- endif %}
{%- endfor %}
<th name="ext"><span>T</span></th>
<th name="ts"><span>Date</span></th>
</tr>
</thead>
<tbody>
{%- for f in files %}
<tr><td>{{ f.lead }}</td><td><a href="{{ f.href }}">{{ f.name|e }}</a></td><td>{{ f.sz }}</td>
{%- if f.tags is defined %}
{%- for k in taglist %}
<td>{{ f.tags[k] }}</td>
{%- endfor %}
{%- endif %}
<td>{{ f.ext }}</td><td>{{ f.dt }}</td></tr>
<tr><td>{{ f.lead }}</td><td><a href="{{ f.href }}">{{ f.name|e }}</a></td><td>{{ f.sz }}</td>
{%- if f.tags is defined %}
{%- for k in taglist %}
<td>{{ f.tags[k] }}</td>
{%- endfor %}
{%- endif %}
<td>{{ f.ext }}</td><td>{{ f.dt }}</td></tr>
{%- endfor %}
</tbody>
</table>
<div id="epi" class="logue">{{ logues[1] }}</div>
</tbody>
</table>
<div id="epi" class="logue">{{ logues[1] }}</div>
<h2><a href="?h">control-panel</a></h2>
<h2><a href="?h">control-panel</a></h2>
</div>
{%- if srv_info %}
<div id="srv_info"><span>{{ srv_info }}</span></div>
{%- endif %}
{%- if srv_info %}
<div id="srv_info"><span>{{ srv_info }}</span></div>
{%- endif %}
<div id="widget"></div>
<div id="widget"></div>
<script>
var tag_order_cfg = {{ tag_order }};
</script>
<script src="/.cpr/util.js{{ ts }}"></script>
<script src="/.cpr/browser.js{{ ts }}"></script>
<script src="/.cpr/up2k.js{{ ts }}"></script>
<script>
apply_perms({{ perms }});
</script>
<script>
var perms = {{ perms }},
tag_order_cfg = {{ tag_order }},
have_up2k_idx = {{ have_up2k_idx|tojson }},
have_tags_idx = {{ have_tags_idx|tojson }},
have_zip = {{ have_zip|tojson }};
</script>
<script src="/.cpr/util.js{{ ts }}"></script>
<script src="/.cpr/browser.js{{ ts }}"></script>
<script src="/.cpr/up2k.js{{ ts }}"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -2,59 +2,59 @@
<html lang="en">
<head>
<meta charset="utf-8">
<title>{{ title }}</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=0.8">
<style>
html{font-family:sans-serif}
td{border:1px solid #999;border-width:1px 1px 0 0;padding:0 5px}
a{display:block}
</style>
<meta charset="utf-8">
<title>{{ title }}</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=0.8">
<style>
html{font-family:sans-serif}
td{border:1px solid #999;border-width:1px 1px 0 0;padding:0 5px}
a{display:block}
</style>
</head>
<body>
{%- if srv_info %}
<p><span>{{ srv_info }}</span></p>
{%- endif %}
{%- if srv_info %}
<p><span>{{ srv_info }}</span></p>
{%- endif %}
{%- if have_b_u %}
<form method="post" enctype="multipart/form-data" accept-charset="utf-8" action="{{ url_suf }}">
<input type="hidden" name="act" value="bput" />
<input type="file" name="f" multiple /><br />
<input type="submit" value="start upload" />
</form>
<br />
{%- endif %}
{%- if have_b_u %}
<form method="post" enctype="multipart/form-data" accept-charset="utf-8" action="{{ url_suf }}">
<input type="hidden" name="act" value="bput" />
<input type="file" name="f" multiple /><br />
<input type="submit" value="start upload" />
</form>
<br />
{%- endif %}
{%- if logues[0] %}
<div>{{ logues[0] }}</div><br />
{%- endif %}
{%- if logues[0] %}
<div>{{ logues[0] }}</div><br />
{%- endif %}
<table id="files">
<thead>
<tr>
<th name="lead"><span>c</span></th>
<th name="href"><span>File Name</span></th>
<th name="sz" sort="int"><span>Size</span></th>
<th name="ts"><span>Date</span></th>
</tr>
</thead>
<tbody>
<tr><td></td><td><a href="../{{ url_suf }}">parent folder</a></td><td>-</td><td>-</td></tr>
<table id="files">
<thead>
<tr>
<th name="lead"><span>c</span></th>
<th name="href"><span>File Name</span></th>
<th name="sz" sort="int"><span>Size</span></th>
<th name="ts"><span>Date</span></th>
</tr>
</thead>
<tbody>
<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 }}">{{ f.name|e }}</a></td><td>{{ f.sz }}</td><td>{{ f.dt }}</td></tr>
{%- endfor %}
</tbody>
</table>
{%- if logues[1] %}
<div>{{ logues[1] }}</div><br />
{%- endif %}
<h2><a href="{{ url_suf }}{{ url_suf and '&amp;' or '?' }}h">control-panel</a></h2>
</tbody>
</table>
{%- if logues[1] %}
<div>{{ logues[1] }}</div><br />
{%- endif %}
<h2><a href="{{ url_suf }}{{ url_suf and '&amp;' or '?' }}h">control-panel</a></h2>
</body>
</html>

View File

@@ -0,0 +1,61 @@
var ofun = audio_eq.apply.bind(audio_eq);
audio_eq.apply = function () {
var ac1 = mp.ac;
ofun();
var ac = mp.ac,
w = 2048,
h = 256;
if (!audio_eq.filters.length) {
audio_eq.ana = null;
return;
}
var can = ebi('fft_can');
if (!can) {
can = mknod('canvas');
can.setAttribute('id', 'fft_can');
can.style.cssText = 'position:absolute;left:0;bottom:5em;width:' + w + 'px;height:' + h + 'px;z-index:9001';
document.body.appendChild(can);
can.width = w;
can.height = h;
}
var cc = can.getContext('2d');
if (!ac)
return;
var ana = ac.createAnalyser();
ana.smoothingTimeConstant = 0;
ana.fftSize = 8192;
audio_eq.filters[0].connect(ana);
audio_eq.ana = ana;
var buf = new Uint8Array(ana.frequencyBinCount),
colw = can.width / buf.length;
cc.fillStyle = '#fc0';
function draw() {
if (ana == audio_eq.ana)
requestAnimationFrame(draw);
ana.getByteFrequencyData(buf);
cc.clearRect(0, 0, can.width, can.height);
/*var x = 0, w = 1;
for (var a = 0; a < buf.length; a++) {
cc.fillRect(x, h - buf[a], w, h);
x += w;
}*/
var mul = Math.pow(w, 4) / buf.length;
for (var x = 0; x < w; x++) {
var a = Math.floor(Math.pow(x, 4) / mul),
v = buf[a];
cc.fillRect(x, h - v, 1, v);
}
}
draw();
};
audio_eq.apply();

View File

@@ -59,7 +59,7 @@
<h1>login for more:</h1>
<ul>
<form method="post" enctype="multipart/form-data" action="/{{ url_suf }}">
<form method="post" enctype="multipart/form-data" action="/">
<input type="hidden" name="act" value="login" />
<input type="password" name="cppwd" />
<input type="submit" value="Login" />

View File

@@ -444,8 +444,7 @@ function up2k_init(subtle) {
}
// show uploader if the user only has write-access
var perms = document.body.getAttribute('perms');
if (perms && !has(perms.split(' '), 'read'))
if (perms.length && !has(perms, 'read'))
goto('up2k');
// shows or clears a message in the basic uploader ui
@@ -741,9 +740,17 @@ function up2k_init(subtle) {
function handshakes_permitted() {
var lim = multitask ? 1 : 0;
return lim >=
if (lim <
st.todo.upload.length +
st.busy.upload.length;
st.busy.upload.length)
return false;
var cd = st.todo.handshake.length ? st.todo.handshake[0].cooldown : 0;
if (cd && cd - Date.now() > 0)
return false;
return true;
}
function hashing_permitted() {
@@ -804,6 +811,14 @@ function up2k_init(subtle) {
var mou_ikkai = false;
if (st.busy.handshake.length > 0 &&
st.busy.handshake[0].busied < Date.now() - 30 * 1000
) {
console.log("retrying stuck handshake");
var t = st.busy.handshake.shift();
st.todo.handshake.unshift(t);
}
if (st.todo.handshake.length > 0 &&
st.busy.handshake.length == 0 && (
st.todo.handshake[0].t4 || (
@@ -963,8 +978,8 @@ function up2k_init(subtle) {
while (segm_next());
var hash_done = function (hashbuf) {
var hslice = new Uint8Array(hashbuf).subarray(0, 32),
b64str = buf2b64(hslice).replace(/=$/, '');
var hslice = new Uint8Array(hashbuf).subarray(0, 33),
b64str = buf2b64(hslice);
hashtab[nch] = b64str;
t.hash.push(nch);
@@ -989,6 +1004,7 @@ function up2k_init(subtle) {
pvis.seth(t.n, 1, '📦 wait');
st.busy.hash.splice(st.busy.hash.indexOf(t), 1);
st.todo.handshake.push(t);
tasker();
};
if (subtle)
@@ -1019,11 +1035,28 @@ function up2k_init(subtle) {
//
function exec_handshake() {
var t = st.todo.handshake.shift();
var t = st.todo.handshake.shift(),
me = Date.now();
st.busy.handshake.push(t);
t.busied = me;
var xhr = new XMLHttpRequest();
xhr.onerror = function () {
if (t.busied != me) {
console.log('zombie handshake onerror,', t);
return;
}
console.log('handshake onerror, retrying');
st.busy.handshake.splice(st.busy.handshake.indexOf(t), 1);
st.todo.handshake.unshift(t);
tasker();
};
xhr.onload = function (e) {
if (t.busied != me) {
console.log('zombie handshake onload,', t);
return;
}
if (xhr.status == 200) {
var response = JSON.parse(xhr.responseText);
@@ -1130,6 +1163,15 @@ function up2k_init(subtle) {
if (rsp.indexOf('<pre>') === 0)
rsp = rsp.slice(5);
if (rsp.indexOf('rate-limit ') !== -1) {
var penalty = rsp.replace(/.*rate-limit /, "").split(' ')[0];
console.log("rate-limit: " + penalty);
t.cooldown = Date.now() + parseFloat(penalty) * 1000;
st.busy.handshake.splice(st.busy.handshake.indexOf(t), 1);
st.todo.handshake.unshift(t);
return;
}
st.bytes.uploaded += t.size;
if (rsp.indexOf('partial upload exists') !== -1 ||
rsp.indexOf('file already exists') !== -1) {
@@ -1241,7 +1283,7 @@ function up2k_init(subtle) {
fpx = parseInt(getComputedStyle(bar)['font-size']),
wem = wpx * 1.0 / fpx,
wide = wem > 54,
parent = ebi(wide ? 'u2btn_cw' : 'u2btn_ct'),
parent = ebi(wide && has(perms, 'write') ? 'u2btn_cw' : 'u2btn_ct'),
btn = ebi('u2btn');
//console.log([wpx, fpx, wem]);
@@ -1254,31 +1296,18 @@ function up2k_init(subtle) {
window.addEventListener('resize', onresize);
onresize();
function desc_show(e) {
var cfg = sread('tooltips');
if (cfg !== null && cfg != '1')
return;
var msg = this.getAttribute('alt'),
cdesc = ebi('u2cdesc');
cdesc.innerHTML = msg.replace(/\$N/g, "<br />");
cdesc.setAttribute('class', 'show');
if (is_touch) {
// android-chrome wobbles for a bit; firefox / iOS-safari are OK
setTimeout(onresize, 20);
setTimeout(onresize, 100);
setTimeout(onresize, 500);
}
function desc_hide(e) {
ebi('u2cdesc').setAttribute('class', '');
}
var o = QSA('#u2conf *[alt]');
var o = QSA('#u2conf *[tt]');
for (var a = o.length - 1; a >= 0; a--) {
o[a].parentNode.getElementsByTagName('input')[0].setAttribute('alt', o[a].getAttribute('alt'));
}
var o = QSA('#u2conf *[alt]');
for (var a = 0; a < o.length; a++) {
o[a].onfocus = desc_show;
o[a].onblur = desc_hide;
o[a].onmouseenter = desc_show;
o[a].onmouseleave = desc_hide;
o[a].parentNode.getElementsByTagName('input')[0].setAttribute('tt', o[a].getAttribute('tt'));
}
tt.init();
function bumpthread(dir) {
try {
@@ -1326,14 +1355,12 @@ function up2k_init(subtle) {
}
function set_fsearch(new_state) {
var perms = document.body.getAttribute('perms'),
fixed = false;
var fixed = false;
if (!ebi('fsearch')) {
new_state = false;
}
else if (perms) {
perms = perms.split(' ');
else if (perms.length) {
if (!has(perms, 'write')) {
new_state = true;
fixed = true;
@@ -1363,6 +1390,8 @@ function up2k_init(subtle) {
ebi('u2bm').innerHTML = ico + ' <sup>' + desc + '</sup>';
}
catch (ex) { }
onresize();
}
function tgl_flag_en() {
@@ -1426,5 +1455,9 @@ function warn_uploader_busy(e) {
}
tt.init();
if (QS('#op_up2k.act'))
goto_up2k();
apply_perms(perms);

View File

@@ -211,29 +211,6 @@
box-shadow: none;
opacity: .2;
}
#u2cdesc {
position: absolute;
width: 34em;
left: calc(50% - 15em);
background: #222;
border: 0 solid #555;
text-align: center;
overflow: hidden;
margin: 0 -2em;
padding: 0 1em;
height: 0;
opacity: .1;
transition: all 0.14s ease-in-out;
box-shadow: 0 .2em .5em #222;
border-radius: .4em;
z-index: 1;
}
#u2cdesc.show {
padding: 1em;
height: auto;
border-width: .2em 0;
opacity: 1;
}
#u2foot {
color: #fff;
font-style: italic;
@@ -286,10 +263,6 @@ html.light #u2conf .txtbox.err {
background: #f96;
color: #300;
}
html.light #u2cdesc {
background: #fff;
border: none;
}
html.light #op_up2k.srch #u2btn {
border-color: #a80;
}

View File

@@ -1,103 +0,0 @@
<div id="op_bup" class="opview opbox act">
<div id="u2err"></div>
<form method="post" enctype="multipart/form-data" accept-charset="utf-8" action="{{ url_suf }}">
<input type="hidden" name="act" value="bput" />
<input type="file" name="f" multiple><br />
<input type="submit" value="start upload">
</form>
</div>
<div id="op_mkdir" class="opview opbox act">
<form method="post" enctype="multipart/form-data" accept-charset="utf-8" action="{{ url_suf }}">
<input type="hidden" name="act" value="mkdir" />
<input type="text" name="name" size="30">
<input type="submit" value="mkdir">
</form>
</div>
<div id="op_new_md" class="opview opbox">
<form method="post" enctype="multipart/form-data" accept-charset="utf-8" action="{{ url_suf }}">
<input type="hidden" name="act" value="new_md" />
<input type="text" name="name" size="30">
<input type="submit" value="create doc">
</form>
</div>
<div id="op_msg" class="opview opbox act">
<form method="post" enctype="application/x-www-form-urlencoded" accept-charset="utf-8" action="{{ url_suf }}">
<input type="text" name="msg" size="30">
<input type="submit" value="send msg">
</form>
</div>
<div id="op_up2k" class="opview">
<form id="u2form" method="post" enctype="multipart/form-data" onsubmit="return false;"></form>
<table id="u2conf">
<tr>
<td><br />parallel uploads:</td>
<td rowspan="2">
<input type="checkbox" id="multitask" />
<label for="multitask" alt="continue hashing other files while uploading">🏃</label>
</td>
<td rowspan="2">
<input type="checkbox" id="ask_up" />
<label for="ask_up" alt="ask for confirmation befofre upload starts">💭</label>
</td>
<td rowspan="2">
<input type="checkbox" id="flag_en" />
<label for="flag_en" alt="ensure only one tab is uploading at a time $N (other tabs must have this enabled too)">💤</label>
</td>
{%- if have_up2k_idx %}
<td data-perm="read" rowspan="2">
<input type="checkbox" id="fsearch" />
<label for="fsearch" alt="don't actually upload, instead check if the files already $N exist on the server (will scan all folders you can read)">🔎</label>
</td>
{%- endif %}
<td data-perm="read" rowspan="2" id="u2btn_cw"></td>
</tr>
<tr>
<td>
<a href="#" id="nthread_sub">&ndash;</a><input
class="txtbox" id="nthread" value="2"/><a
href="#" id="nthread_add">+</a><br />&nbsp;
</td>
</tr>
</table>
<div id="u2cdesc"></div>
<div id="u2notbtn"></div>
<div id="u2btn_ct">
<div id="u2btn">
<span id="u2bm"></span><br />
drag/drop files<br />
and folders here<br />
(or click me)
</div>
</div>
<div id="u2cards">
<a href="#" act="ok">ok <span>0</span></a><a
href="#" act="ng">ng <span>0</span></a><a
href="#" act="done">done <span>0</span></a><a
href="#" act="bz" class="act">busy <span>0</span></a><a
href="#" act="q">que <span>0</span></a>
</div>
<table id="u2tab">
<thead>
<tr>
<td>filename</td>
<td>status</td>
<td>progress<a href="#" id="u2cleanup">cleanup</a></td>
</tr>
</thead>
<tbody></tbody>
</table>
<p id="u2foot"></p>
<p id="u2footfoot" data-perm="write">( you can use the <a href="#" id="u2nope">basic uploader</a> if you don't need lastmod timestamps, resumable uploads, or progress bars )</p>
</div>

View File

@@ -6,7 +6,7 @@ if (!window['console'])
};
var clickev = window.Touch ? 'touchstart' : 'click',
var is_touch = 'ontouchstart' in window,
ANDROID = /(android)/i.test(navigator.userAgent);
@@ -285,63 +285,6 @@ function makeSortable(table, cb) {
}
(function () {
var ops = QSA('#ops>a');
for (var a = 0; a < ops.length; a++) {
ops[a].onclick = opclick;
}
})();
function opclick(e) {
ev(e);
var dest = this.getAttribute('data-dest');
goto(dest);
swrite('opmode', dest || null);
var input = QS('.opview.act input:not([type="hidden"])')
if (input)
input.focus();
}
function goto(dest) {
var obj = QSA('.opview.act');
for (var a = obj.length - 1; a >= 0; a--)
clmod(obj[a], 'act');
obj = QSA('#ops>a');
for (var a = obj.length - 1; a >= 0; a--)
clmod(obj[a], 'act');
if (dest) {
var ui = ebi('op_' + dest);
clmod(ui, 'act', true);
QS('#ops>a[data-dest=' + dest + ']').className += " act";
var fn = window['goto_' + dest];
if (fn)
fn();
}
if (window['treectl'])
treectl.onscroll();
}
(function () {
goto();
var op = sread('opmode');
if (op !== null && op !== '.')
try {
goto(op);
}
catch (ex) { }
})();
function linksplit(rp) {
var ret = [];
var apath = '/';
@@ -416,6 +359,15 @@ function get_vpath() {
}
function get_pwd() {
var pwd = ('; ' + document.cookie).split('; cppwd=');
if (pwd.length < 2)
return null;
return pwd[1].split(';')[0];
}
function unix2iso(ts) {
return new Date(ts * 1000).toISOString().replace("T", " ").slice(0, -5);
}
@@ -528,3 +480,67 @@ function hist_replace(url) {
console.log("h-repl " + url);
history.replaceState(url, url, url);
}
var tt = (function () {
var r = {
"tt": mknod("div"),
"en": true
};
r.tt.setAttribute('id', 'tt');
document.body.appendChild(r.tt);
function show() {
var cfg = sread('tooltips');
if (cfg !== null && cfg != '1')
return;
var msg = this.getAttribute('tt');
if (!msg)
return;
var pos = this.getBoundingClientRect(),
left = pos.left < window.innerWidth / 2,
top = pos.top < window.innerHeight / 2;
r.tt.style.top = top ? pos.bottom + 'px' : 'auto';
r.tt.style.bottom = top ? 'auto' : (window.innerHeight - pos.top) + 'px';
r.tt.style.left = left ? pos.left + 'px' : 'auto';
r.tt.style.right = left ? 'auto' : (window.innerWidth - pos.right) + 'px';
r.tt.innerHTML = msg.replace(/\$N/g, "<br />");
clmod(r.tt, 'show', 1);
}
function hide() {
clmod(r.tt, 'show');
}
r.init = function () {
var ttb = ebi('tooltips');
if (ttb) {
ttb.onclick = function (e) {
ev(e);
r.en = !r.en;
bcfg_set('tooltips', r.en);
r.init();
};
r.en = bcfg_get('tooltips', true)
}
var _show = r.en ? show : null,
_hide = r.en ? hide : null;
var o = QSA('*[tt]');
for (var a = o.length - 1; a >= 0; a--) {
o[a].onfocus = _show;
o[a].onblur = _hide;
o[a].onmouseenter = _show;
o[a].onmouseleave = _hide;
}
hide();
};
return r;
})();

View File

@@ -1,8 +1,19 @@
## [`minimal-up2k.html`](minimal-up2k.html)
* save as `.epilogue.html` inside a folder to [simplify the ui](https://user-images.githubusercontent.com/241032/118311195-dd6ca380-b4ef-11eb-86f3-75a3ff2e1332.png)
# example `.epilogue.html`
save one of these as `.epilogue.html` inside a folder to customize it:
## [`browser.css`](browser.css)
* example for `--css-browser`
* [`minimal-up2k.html`](minimal-up2k.html) will [simplify the upload ui](https://user-images.githubusercontent.com/241032/118311195-dd6ca380-b4ef-11eb-86f3-75a3ff2e1332.png)
# example browser-css
point `--css-browser` to one of these by URL:
* [`browser.css`](browser.css) changes the background
* [`browser-icons.css`](browser-icons.css) adds filetype icons
# other stuff
## [`rclone.md`](rclone.md)
* notes on using rclone as a fuse client/server

95
docs/biquad.html Normal file
View File

@@ -0,0 +1,95 @@
<!DOCTYPE html><html><head></head><body><script>
setTimeout(location.reload.bind(location), 700);
document.documentElement.scrollLeft = 0;
var can = document.createElement('canvas'),
cc = can.getContext('2d'),
w = 2048,
h = 1024;
w = 2048;
can.width = w;
can.height = h;
document.body.appendChild(can);
can.style.cssText = 'width:' + w + 'px;height:' + h + 'px';
cc.fillStyle = '#000';
cc.fillRect(0, 0, w, h);
var cfg = [ // hz, q, g
[31.25 * 0.88, 0, 1.4], // shelf
[31.25 * 1.04, 0.7, 0.96], // peak
[62.5, 0.7, 1],
[125, 0.8, 1],
[250, 0.9, 1.03],
[500, 0.9, 1.1],
[1000, 0.9, 1.1],
[2000, 0.9, 1.105],
[4000, 0.88, 1.05],
[8000 * 1.006, 0.73, 1.24],
//[16000 * 1.00, 0.5, 1.75], // peak.v1
//[16000 * 1.19, 0, 1.8] // shelf.v1
[16000 * 0.89, 0.7, 1.26], // peak
[16000 * 1.13, 0.82, 1.09], // peak
[16000 * 1.205, 0, 1.9] // shelf
];
var freqs = new Float32Array(22000),
sum = new Float32Array(freqs.length),
ac = new AudioContext(),
step = w / freqs.length,
colors = [
'rgba(255, 0, 0, 0.7)',
'rgba(0, 224, 0, 0.7)',
'rgba(0, 64, 255, 0.7)'
];
var order = [];
for (var a = 0; a < cfg.length; a += 2)
order.push(a);
for (var a = 1; a < cfg.length; a += 2)
order.push(a);
for (var ia = 0; ia < order.length; ia++) {
var a = order[ia],
fi = ac.createBiquadFilter(),
mag = new Float32Array(freqs.length),
phase = new Float32Array(freqs.length);
for (var b = 0; b < freqs.length; b++)
freqs[b] = b;
fi.type = a == 0 ? 'lowshelf' : a == cfg.length - 1 ? 'highshelf' : 'peaking';
fi.frequency.value = cfg[a][0];
fi.Q.value = cfg[a][1];
fi.gain.value = 1;
fi.getFrequencyResponse(freqs, mag, phase);
cc.fillStyle = colors[a % colors.length];
for (var b = 0; b < sum.length; b++) {
mag[b] -= 1;
sum[b] += mag[b] * cfg[a][2];
var y = h - (mag[b] * h * 3);
cc.fillRect(b * step, y, step, h - y);
cc.fillRect(b * step - 1, y - 1, 3, 3);
}
}
var min = 999999, max = 0;
for (var a = 0; a < sum.length; a++) {
min = Math.min(min, sum[a]);
max = Math.max(max, sum[a]);
}
cc.fillStyle = 'rgba(255,255,255,1)';
for (var a = 0; a < sum.length; a++) {
var v = (sum[a] - min) / (max - min);
cc.fillRect(a * step, 0, step, v * h / 2);
}
cc.fillRect(0, 460, w, 1);
</script></body></html>

66
docs/browser-icons.css Normal file
View File

@@ -0,0 +1,66 @@
/* put filetype icons inline with text
#ggrid>a>span:before,
#ggrid>a>span.dir:before {
display: inline;
line-height: 0;
font-size: 1.7em;
margin: -.7em .1em -.5em -.6em;
}
*/
/* move folder icons top-left */
#ggrid>a>span.dir:before {
content: initial;
}
#ggrid>a[href$="/"]:before {
content: '📂';
}
/* put filetype icons top-left */
#ggrid>a:before {
display: block;
position: absolute;
padding: .3em 0;
margin: -.4em;
text-shadow: 0 0 .1em #000;
background: linear-gradient(135deg,rgba(255,255,255,0) 50%,rgba(255,255,255,0.2));
border-radius: .3em;
font-size: 2em;
}
/* video */
#ggrid>a:is(
[href$=".mkv"i],
[href$=".mp4"i],
[href$=".webm"i],
):before {
content: '📺';
}
/* audio */
#ggrid>a:is(
[href$=".mp3"i],
[href$=".ogg"i],
[href$=".opus"i],
[href$=".flac"i],
[href$=".m4a"i],
[href$=".aac"i],
):before {
content: '🎵';
}
/* image */
#ggrid>a:is(
[href$=".jpg"i],
[href$=".jpeg"i],
[href$=".png"i],
[href$=".gif"i],
[href$=".webp"i],
):before {
content: '🎨';
}

View File

@@ -1,5 +1,5 @@
html {
background: url('/wp/wallhaven-mdjrqy.jpg') center / cover no-repeat fixed;
background: #333 url('/wp/wallhaven-mdjrqy.jpg') center / cover no-repeat fixed;
}
#files th {
background: rgba(32, 32, 32, 0.9) !important;
@@ -12,7 +12,7 @@ html {
html.light {
background: url('/wp/wallhaven-dpxl6l.png') center / cover no-repeat fixed;
background: #eee url('/wp/wallhaven-dpxl6l.png') center / cover no-repeat fixed;
}
html.light #files th {
background: rgba(255, 255, 255, 0.9) !important;

View File

@@ -86,6 +86,9 @@ var t=[]; var b=document.location.href.split('#')[0].slice(0, -1); document.quer
# get the size and video-id of all youtube vids in folder, assuming filename ends with -id.ext, and create a copyparty search query
find -maxdepth 1 -printf '%s %p\n' | sort -n | awk '!/-([0-9a-zA-Z_-]{11})\.(mkv|mp4|webm)$/{next} {sub(/\.[^\.]+$/,"");n=length($0);v=substr($0,n-10);print $1, v}' | tee /dev/stderr | awk 'BEGIN {p="("} {printf("%s name like *-%s.* ",p,$2);p="or"} END {print ")\n"}' | cat >&2
# unique stacks in a stackdump
f=a; rm -rf stacks; mkdir stacks; grep -E '^#' $f | while IFS= read -r n; do awk -v n="$n" '!$0{o=0} o; $0==n{o=1}' <$f >stacks/f; h=$(sha1sum <stacks/f | cut -c-16); mv stacks/f stacks/$h-"$n"; done ; find stacks/ | sort | uniq -cw24
##
## sqlite3 stuff
@@ -100,6 +103,15 @@ cat warks | while IFS= read -r x; do sqlite3 up2k.db "delete from mt where w = '
# dump all dbs
find -iname up2k.db | while IFS= read -r x; do sqlite3 "$x" 'select substr(w,1,12), rd, fn from up' | sed -r 's/\|/ \| /g' | while IFS= read -r y; do printf '%s | %s\n' "$x" "$y"; done; done
# unschedule mtp scan for all files somewhere under "enc/"
sqlite3 -readonly up2k.db 'select substr(up.w,1,16) from up inner join mt on mt.w = substr(up.w,1,16) where rd like "enc/%" and +mt.k = "t:mtp"' > keys; awk '{printf "delete from mt where w = \"%s\" and +k = \"t:mtp\";\n", $0}' <keys | tee /dev/stderr | sqlite3 up2k.db
# compare metadata key "key" between two databases
sqlite3 -readonly up2k.db.key-full 'select w, v from mt where k = "key" order by w' > k1; sqlite3 -readonly up2k.db 'select w, v from mt where k = "key" order by w' > k2; ok=0; ng=0; while IFS='|' read w k2; do k1="$(grep -E "^$w" k1 | sed -r 's/.*\|//')"; [ "$k1" = "$k2" ] && ok=$((ok+1)) || { ng=$((ng+1)); printf '%3s %3s %s\n' "$k1" "$k2" "$(sqlite3 -readonly up2k.db.key-full "select * from up where substr(w,1,16) = '$w'" | sed -r 's/\|/ | /g')"; }; done < <(cat k2); echo "match $ok diff $ng"
# actually this is much better
sqlite3 -readonly up2k.db.key-full 'select w, v from mt where k = "key" order by w' > k1; sqlite3 -readonly up2k.db 'select mt.w, mt.v, up.rd, up.fn from mt inner join up on mt.w = substr(up.w,1,16) where mt.k = "key" order by up.rd, up.fn' > k2; ok=0; ng=0; while IFS='|' read w k2 path; do k1="$(grep -E "^$w" k1 | sed -r 's/.*\|//')"; [ "$k1" = "$k2" ] && ok=$((ok+1)) || { ng=$((ng+1)); printf '%3s %3s %s\n' "$k1" "$k2" "$path"; }; done < <(cat k2); echo "match $ok diff $ng"
##
## media
@@ -154,7 +166,7 @@ dbg.asyncStore.pendingBreakpoints = {}
about:config >> devtools.debugger.prefs-schema-version = -1
# determine server version
git reset --hard origin/HEAD && git log --format=format:"%H %ai %d" --decorate=full > /dev/shm/revs && cat /dev/shm/revs | while read -r rev extra; do (git reset --hard $rev >/dev/null 2>/dev/null && dsz=$(cat copyparty/web/{util,browser,up2k}.js 2>/dev/null | diff -wNarU0 - <(cat /mnt/Users/ed/Downloads/ref/{util,browser,up2k}.js) | wc -c) && printf '%s %6s %s\n' "$rev" $dsz "$extra") </dev/null; done
git pull; git reset --hard origin/HEAD && git log --format=format:"%H %ai %d" --decorate=full > ../revs && cat ../{util,browser}.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}.js >../vg 2>/dev/null && diff -wNarU0 ../{vg,vr} | wc -c) && printf '%s %6s %s\n' "$rev" $dsz "$extra") </dev/null; done
##
@@ -197,3 +209,4 @@ mk() { rm -rf /tmp/foo; sudo -u ed bash -c 'mkdir /tmp/foo; echo hi > /tmp/foo/b
mk && t0="$(date)" && while true; do date -s "$(date '+ 1 hour')"; systemd-tmpfiles --clean; ls -1 /tmp | grep foo || break; done; echo "$t0"
mk && sudo -u ed flock /tmp/foo sleep 40 & sleep 1; ps aux | grep -E 'sleep 40$' && t0="$(date)" && for n in {1..40}; do date -s "$(date '+ 1 day')"; systemd-tmpfiles --clean; ls -1 /tmp | grep foo || break; done; echo "$t0"
mk && t0="$(date)" && for n in {1..40}; do date -s "$(date '+ 1 day')"; systemd-tmpfiles --clean; ls -1 /tmp | grep foo || break; tar -cf/dev/null /tmp/foo; done; echo "$t0"

32
docs/tcp-debug.sh Normal file
View File

@@ -0,0 +1,32 @@
(cd ~/dev/copyparty && strace -Tttyyvfs 256 -o strace.strace python3 -um copyparty -i 127.0.0.1 --http-only --stackmon /dev/shm/cpps,10 ) 2>&1 | tee /dev/stderr > ~/log-copyparty-$(date +%Y-%m%d-%H%M%S).txt
14/Jun/2021:16:34:02 1623688447.212405 death
14/Jun/2021:16:35:02 1623688502.420860 back
tcpdump -nni lo -w /home/ed/lo.pcap
# 16:35:25.324662 IP 127.0.0.1.48632 > 127.0.0.1.3920: Flags [F.], seq 849, ack 544, win 359, options [nop,nop,TS val 809396796 ecr 809396796], length 0
tcpdump -nnr /home/ed/lo.pcap | awk '/ > 127.0.0.1.3920: /{sub(/ > .*/,"");sub(/.*\./,"");print}' | sort -n | uniq | while IFS= read -r port; do echo; tcpdump -nnr /home/ed/lo.pcap 2>/dev/null | grep -E "\.$port( > |: F)" | sed -r 's/ > .*, /, /'; done | grep -E '^16:35:0.*length [^0]' -C50
16:34:02.441732 IP 127.0.0.1.48638, length 0
16:34:02.441738 IP 127.0.0.1.3920, length 0
16:34:02.441744 IP 127.0.0.1.48638, length 0
16:34:02.441756 IP 127.0.0.1.48638, length 791
16:34:02.441759 IP 127.0.0.1.3920, length 0
16:35:02.445529 IP 127.0.0.1.48638, length 0
16:35:02.489194 IP 127.0.0.1.3920, length 0
16:35:02.515595 IP 127.0.0.1.3920, length 216
16:35:02.515600 IP 127.0.0.1.48638, length 0
grep 48638 "$(find ~ -maxdepth 1 -name log-copyparty-\*.txt | sort | tail -n 1)"
1623688502.510380 48638 rh
1623688502.511291 48638 Unrecv direct ...
1623688502.511827 48638 rh = 791
16:35:02.518 127.0.0.1 48638 shut(8): [Errno 107] Socket not connected
Exception in thread httpsrv-0.1-48638:
grep 48638 ~/dev/copyparty/strace.strace
14561 16:35:02.506310 <... accept4 resumed> {sa_family=AF_INET, sin_port=htons(48638), sin_addr=inet_addr("127.0.0.1")}, [16], SOCK_CLOEXEC) = 8<TCP:[127.0.0.1:3920->127.0.0.1:48638]> <0.000012>
15230 16:35:02.510725 write(1<pipe:[256639555]>, "1623688502.510380 48638 rh\n", 27 <unfinished ...>

View File

@@ -92,20 +92,34 @@ chmod 755 \
copyparty-extras/copyparty-*/{scripts,bin}/*
# extract and repack the sfx with less features enabled
# extract the sfx
( cd copyparty-extras/sfx-full/
./copyparty-sfx.py -h
cd ../copyparty-*/
./scripts/make-sfx.sh re no-ogv no-cm
)
# put new sfx into copyparty-extras/sfx-lite/,
# fuse client into copyparty-extras/,
repack() {
# do the repack
(cd copyparty-extras/copyparty-*/
./scripts/make-sfx.sh $2
)
# put new sfx into copyparty-extras/$name/,
( cd copyparty-extras/
mv copyparty-*/dist/* $1/
)
}
repack sfx-full "re gz no-sh"
repack sfx-lite "re no-ogv no-cm"
repack sfx-lite "re no-ogv no-cm gz no-sh"
# move fuse client into copyparty-extras/,
# copy lite-sfx.py to ./copyparty,
# delete extracted source code
( cd copyparty-extras/
mv copyparty-*/dist/* sfx-lite/
mv copyparty-*/bin/copyparty-fuse.py .
cp -pv sfx-lite/copyparty-sfx.py ../copyparty
rm -rf copyparty-{0..9}*.*.*{0..9}
@@ -119,6 +133,7 @@ true
# create the bundle
printf '\n\n'
fn=copyparty-$(date +%Y-%m%d-%H%M%S).tgz
tar -czvf "$od/$fn" *
cd "$od"

View File

@@ -1,6 +1,7 @@
FROM alpine:3.13
WORKDIR /z
ENV ver_asmcrypto=5b994303a9d3e27e0915f72a10b6c2c51535a4dc \
ver_hashwasm=4.7.0 \
ver_marked=1.1.0 \
ver_ogvjs=1.8.0 \
ver_mde=2.14.0 \
@@ -9,12 +10,6 @@ ENV ver_asmcrypto=5b994303a9d3e27e0915f72a10b6c2c51535a4dc \
ver_zopfli=1.0.3
# TODO
# sha512.hw.js https://github.com/Daninet/hash-wasm
# sha512.kc.js https://github.com/chm-diederichs/sha3-wasm
# awk '/HMAC state/{o=1} /var HEAP/{o=0} /function hmac_reset/{o=1} /return \{/{o=0} /var __extends =/{o=1} /var Hash =/{o=0} /hmac_|pbkdf2_/{next} o{next} {gsub(/IllegalStateError/,"Exception")} {sub(/^ +/,"");sub(/^\/\/ .*/,"");sub(/;$/," ;")} 1' <sha512.ac.js.orig >sha512.ac.js; for fn in sha512.ac.js.orig sha512.ac.js; do wc -c <$fn; wc -c <$fn.gz ; for n in {1..9}; do printf '%8d %d bz\n' $(bzip2 -c$n <$fn | wc -c) $n; done; done
# download;
# the scp url is latin from https://fonts.googleapis.com/css2?family=Source+Code+Pro&display=swap
RUN mkdir -p /z/dist/no-pk \
@@ -27,7 +22,11 @@ RUN mkdir -p /z/dist/no-pk \
&& wget https://github.com/codemirror/CodeMirror/archive/$ver_codemirror.tar.gz -O codemirror.tgz \
&& wget https://github.com/FortAwesome/Font-Awesome/releases/download/$ver_fontawesome/fontawesome-free-$ver_fontawesome-web.zip -O fontawesome.zip \
&& wget https://github.com/google/zopfli/archive/zopfli-$ver_zopfli.tar.gz -O zopfli.tgz \
&& wget https://github.com/Daninet/hash-wasm/releases/download/v$ver_hashwasm/hash-wasm@$ver_hashwasm.zip -O hash-wasm.zip \
&& unzip ogvjs.zip \
&& (mkdir hash-wasm \
&& cd hash-wasm \
&& unzip ../hash-wasm.zip) \
&& (tar -xf asmcrypto.tgz \
&& cd asmcrypto.js-$ver_asmcrypto \
&& npm install ) \
@@ -64,7 +63,12 @@ RUN tar -xf zopfli.tgz \
RUN cd asmcrypto.js-$ver_asmcrypto \
&& echo "export { Sha512 } from './hash/sha512/sha512';" > src/entry-export_all.ts \
&& node -r esm build.js \
&& mv asmcrypto.all.es5.js /z/dist/sha512.js
&& awk '/HMAC state/{o=1} /var HEAP/{o=0} /function hmac_reset/{o=1} /return \{/{o=0} /var __extends =/{o=1} /var Hash =/{o=0} /hmac_|pbkdf2_/{next} o{next} {gsub(/IllegalStateError/,"Exception")} {sub(/^ +/,"");sub(/^\/\/ .*/,"");sub(/;$/," ;")} 1' < asmcrypto.all.es5.js > /z/dist/sha512.ac.js
# build hash-wasm
RUN cd hash-wasm \
&& mv sha512.umd.min.js /z/dist/sha512.hw.js
# build ogvjs

View File

@@ -11,6 +11,10 @@ echo
# `re` does a repack of an sfx which you already executed once
# (grabs files from the sfx-created tempdir), overrides `clean`
#
# `gz` creates a gzip-compressed python sfx instead of bzip2
#
# `no-sh` makes just the python sfx, skips the sh/unix sfx
#
# `no-ogv` saves ~500k by removing the opus/vorbis audio codecs
# (only affects apple devices; everything else has native support)
#
@@ -167,7 +171,7 @@ find .. -type f \( -name .DS_Store -or -name ._.DS_Store \) -delete
find .. -type f -name ._\* | while IFS= read -r f; do cmp <(printf '\x00\x05\x16') <(head -c 3 -- "$f") && rm -f -- "$f"; done
echo use smol web deps
rm -f copyparty/web/deps/*.full.* copyparty/web/Makefile
rm -f copyparty/web/deps/*.full.* copyparty/web/dbg-* copyparty/web/Makefile
# it's fine dw
grep -lE '\.full\.(js|css)' copyparty/web/* |

105
scripts/test/race.py Normal file
View File

@@ -0,0 +1,105 @@
#!/usr/bin/env python3
import os
import sys
import time
import json
import threading
import http.client
class Conn(object):
def __init__(self, ip, port):
self.s = http.client.HTTPConnection(ip, port, timeout=260)
self.st = []
def get(self, vpath):
self.st = [time.time()]
self.s.request("GET", vpath)
self.st.append(time.time())
ret = self.s.getresponse()
self.st.append(time.time())
if ret.status < 200 or ret.status >= 400:
raise Exception(ret.status)
ret = ret.read()
self.st.append(time.time())
return ret
def get_json(self, vpath):
ret = self.get(vpath)
return json.loads(ret)
class CState(threading.Thread):
def __init__(self, cs):
threading.Thread.__init__(self)
self.daemon = True
self.cs = cs
self.start()
def run(self):
colors = [5, 1, 3, 2, 7]
remotes = []
remotes_ok = False
while True:
time.sleep(0.001)
if not remotes_ok:
remotes = []
remotes_ok = True
for conn in self.cs:
try:
remotes.append(conn.s.sock.getsockname()[1])
except:
remotes.append("?")
remotes_ok = False
m = []
for conn, remote in zip(self.cs, remotes):
stage = len(conn.st)
m.append(f"\033[3{colors[stage]}m{remote}")
m = " ".join(m)
print(f"{m}\033[0m\n\033[A", end="")
def allget(cs, urls):
thrs = []
for c, url in zip(cs, urls):
t = threading.Thread(target=c.get, args=(url,))
t.start()
thrs.append(t)
for t in thrs:
t.join()
def main():
os.system("")
ip, port = sys.argv[1].split(":")
port = int(port)
cs = []
for _ in range(64):
cs.append(Conn(ip, 3923))
CState(cs)
urlbase = "/doujin/c95"
j = cs[0].get_json(f"{urlbase}?ls")
urls = []
for d in j["dirs"]:
urls.append(f"{urlbase}/{d['href']}?th=w")
for n in range(100):
print(n)
allget(cs, urls)
if __name__ == "__main__":
main()

View File

@@ -28,6 +28,7 @@ class Cfg(Namespace):
a=a,
v=v,
c=c,
rproxy=0,
ed=False,
no_zip=False,
no_scandir=False,

View File

@@ -24,6 +24,7 @@ class Cfg(Namespace):
"hist": None,
"no_hash": False,
"css_browser": None,
"rproxy": 0,
}
ex.update(ex2)
super(Cfg, self).__init__(a=a, v=v, c=c, **ex)