mirror of
https://github.com/9001/copyparty.git
synced 2025-10-24 08:33:58 +00:00
Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c80307e8ff | ||
|
|
c1d77e1041 | ||
|
|
d9e83650dc | ||
|
|
f6d635acd9 | ||
|
|
0dbd8a01ff | ||
|
|
8d755d41e0 | ||
|
|
190473bd32 | ||
|
|
030d1ec254 | ||
|
|
5a2b91a084 | ||
|
|
a50a05e4e7 | ||
|
|
6cb5a87c79 | ||
|
|
b9f89ca552 | ||
|
|
26c9fd5dea | ||
|
|
e81a9b6fe0 | ||
|
|
452450e451 | ||
|
|
419dd2d1c7 | ||
|
|
ee86b06676 | ||
|
|
953183f16d | ||
|
|
228f71708b | ||
|
|
621471a7cb | ||
|
|
8b58e951e3 | ||
|
|
1db489a0aa | ||
|
|
be65c3c6cf | ||
|
|
46e7fa31fe | ||
|
|
66e21bd499 | ||
|
|
8cab4c01fd | ||
|
|
d52038366b | ||
|
|
4fcfd87f5b | ||
|
|
f893c6baa4 | ||
|
|
9a45549b66 |
65
README.md
65
README.md
@@ -39,9 +39,10 @@ turn your phone or raspi into a portable file server with resumable uploads/down
|
||||
* [batch rename](#batch-rename)
|
||||
* [markdown viewer](#markdown-viewer)
|
||||
* [other tricks](#other-tricks)
|
||||
* [searching](#searching)
|
||||
* [search configuration](#search-configuration)
|
||||
* [database location](#database-location)
|
||||
* [searching](#searching)
|
||||
* [server config](#server-config)
|
||||
* [upload rules](#upload-rules)
|
||||
* [database location](#database-location)upload rules
|
||||
* [metadata from audio files](#metadata-from-audio-files)
|
||||
* [file parser plugins](#file-parser-plugins)
|
||||
* [complete examples](#complete-examples)
|
||||
@@ -431,7 +432,7 @@ the metadata keys you can use in the format field are the ones in the file-brows
|
||||
* 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
|
||||
## searching
|
||||
|
||||

|
||||
|
||||
@@ -443,12 +444,12 @@ path/name queries are space-separated, AND'ed together, and words are negated wi
|
||||
* path: `shibayan -bossa` finds all files where one of the folders contain `shibayan` but filters out any results where `bossa` exists somewhere in the path
|
||||
* name: `demetori styx` gives you [good stuff](https://www.youtube.com/watch?v=zGh0g14ZJ8I&list=PL3A147BD151EE5218&index=9)
|
||||
|
||||
add `-e2ts` to also scan/index tags from music files:
|
||||
add the argument `-e2ts` to also scan/index tags from music files, which brings us over to:
|
||||
|
||||
|
||||
## search configuration
|
||||
# server config
|
||||
|
||||
searching relies on two databases, the up2k filetree (`-e2d`) and the metadata tags (`-e2t`). Configuration can be done through arguments, volume flags, or a mix of both.
|
||||
file indexing relies on two databases, the up2k filetree (`-e2d`) and the metadata tags (`-e2t`). Configuration can be done through arguments, volume flags, or a mix of both.
|
||||
|
||||
through arguments:
|
||||
* `-e2d` enables file indexing on upload
|
||||
@@ -467,12 +468,52 @@ note:
|
||||
* `e2tsr` is probably always overkill, since `e2ds`/`e2dsa` would pick up any file modifications and `e2ts` would then reindex those, unless there is a new copyparty version with new parsers and the release note says otherwise
|
||||
* 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:
|
||||
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 `:c,dhash`, this has the following consequences:
|
||||
* initial indexing is way faster, especially when the volume is on a network disk
|
||||
* makes it impossible to [file-search](#file-search)
|
||||
* if someone uploads the same file contents, the upload will not be detected as a dupe, so it will not get symlinked or rejected
|
||||
|
||||
if you set `--no-hash`, you can enable hashing for specific volumes using flag `cehash`
|
||||
if you set `--no-hash`, you can enable hashing for specific volumes using flag `:c,ehash`
|
||||
|
||||
|
||||
## upload rules
|
||||
|
||||
you can set upload rules using volume flags, some examples:
|
||||
|
||||
* `:c,sz=1k-3m` sets allowed filesize between 1 KiB and 3 MiB inclusive (suffixes: b, k, m, g)
|
||||
* `:c,nosub` disallow uploading into subdirectories; goes well with `rotn` and `rotf`:
|
||||
* `:c,rotn=1000,2` moves uploads into subfolders, up to 1000 files in each folder before making a new one, two levels deep (must be at least 1)
|
||||
* `:c,rotf=%Y/%m/%d/%H` enforces files to be uploaded into a structure of subfolders according to that date format
|
||||
* if someone uploads to `/foo/bar` the path would be rewritten to `/foo/bar/2021/08/06/23` for example
|
||||
* but the actual value is not verified, just the structure, so the uploader can choose any values which conform to the format string
|
||||
* just to avoid additional complexity in up2k which is enough of a mess already
|
||||
* `:c,lifetime=300` delete uploaded files when they become 5 minutes old
|
||||
|
||||
you can also set transaction limits which apply per-IP and per-volume, but these assume `-j 1` (default) otherwise the limits will be off, for example `-j 4` would allow anywhere between 1x and 4x the limits you set depending on which processing node the client gets routed to
|
||||
|
||||
* `:c,maxn=250,3600` allows 250 files over 1 hour from each IP (tracked per-volume)
|
||||
* `:c,maxb=1g,300` allows 1 GiB total over 5 minutes from each IP (tracked per-volume)
|
||||
|
||||
|
||||
## compress uploads
|
||||
|
||||
files can be autocompressed on upload, either on user-request (if config allows) or forced by server-config
|
||||
|
||||
* volume flag `gz` allows gz compression
|
||||
* volume flag `xz` allows lzma compression
|
||||
* volume flag `pk` **forces** compression on all files
|
||||
* url parameter `pk` requests compression with server-default algorithm
|
||||
* url parameter `gz` or `xz` requests compression with a specific algorithm
|
||||
* url parameter `xz` requests xz compression
|
||||
|
||||
things to note,
|
||||
* the `gz` and `xz` arguments take a single optional argument, the compression level (range 0 to 9)
|
||||
* the `pk` volume flag takes the optional argument `ALGORITHM,LEVEL` which will then be forced for all uploads, for example `gz,9` or `xz,0`
|
||||
* default compression is gzip level 9
|
||||
* all upload methods except up2k are supported
|
||||
* the files will be indexed after compression, so dupe-detection and file-search will not work as expected
|
||||
|
||||
some examples,
|
||||
|
||||
|
||||
## database location
|
||||
@@ -581,11 +622,11 @@ quick summary of more eccentric web-browsers trying to view a directory index:
|
||||
* `var xhr = new XMLHttpRequest(); xhr.open('POST', 'https://127.0.0.1:3923/msgs?raw'); xhr.send('foo');`
|
||||
|
||||
* curl/wget: upload some files (post=file, chunk=stdin)
|
||||
* `post(){ curl -b cppwd=wark http://127.0.0.1:3923/ -F act=bput -F f=@"$1";}`
|
||||
* `post(){ curl -b cppwd=wark -F act=bput -F f=@"$1" http://127.0.0.1:3923/;}`
|
||||
`post movie.mkv`
|
||||
* `post(){ wget --header='Cookie: cppwd=wark' http://127.0.0.1:3923/?raw --post-file="$1" -O-;}`
|
||||
* `post(){ wget --header='Cookie: cppwd=wark' --post-file="$1" -O- http://127.0.0.1:3923/?raw;}`
|
||||
`post movie.mkv`
|
||||
* `chunk(){ curl -b cppwd=wark http://127.0.0.1:3923/ -T-;}`
|
||||
* `chunk(){ curl -b cppwd=wark -T- http://127.0.0.1:3923/;}`
|
||||
`chunk <movie.mkv`
|
||||
|
||||
* FUSE: mount a copyparty server as a local filesystem
|
||||
|
||||
@@ -61,3 +61,8 @@ cd /mnt/nas/music/.hist
|
||||
~/src/copyparty/bin/dbtool.py -src up2k.*.v3 up2k.db -rm-mtp-flag -copy key
|
||||
~/src/copyparty/bin/dbtool.py -src up2k.*.v3 up2k.db -rm-mtp-flag -copy .bpm -vac
|
||||
```
|
||||
|
||||
|
||||
# [`prisonparty.sh`](prisonparty.sh)
|
||||
* run copyparty in a chroot, preventing any accidental file access
|
||||
* creates bindmounts for /bin, /lib, and so on, see `sysdirs=`
|
||||
|
||||
39
bin/mtag/res/yt-ipr.conf
Normal file
39
bin/mtag/res/yt-ipr.conf
Normal file
@@ -0,0 +1,39 @@
|
||||
# example config file to use copyparty as a youtube manifest collector,
|
||||
# use with copyparty like: python copyparty.py -c yt-ipr.conf
|
||||
#
|
||||
# see docs/example.conf for a better explanation of the syntax, but
|
||||
# newlines are block separators, so adding blank lines inside a volume definition is bad
|
||||
# (use comments as separators instead)
|
||||
|
||||
|
||||
# create user ed, password wark
|
||||
u ed:wark
|
||||
|
||||
|
||||
# create a volume at /ytm which stores files at ./srv/ytm
|
||||
./srv/ytm
|
||||
/ytm
|
||||
# write-only, but read-write for user ed
|
||||
w
|
||||
rw ed
|
||||
# rescan the volume on startup
|
||||
c e2dsa
|
||||
# collect tags from all new files since last scan
|
||||
c e2ts
|
||||
# optionally enable compression to make the files 50% smaller
|
||||
c pk
|
||||
# only allow uploads which are between 16k and 1m large
|
||||
c sz=16k-1m
|
||||
# allow up to 10 uploads over 5 minutes from each ip
|
||||
c maxn=10,300
|
||||
# move uploads into subfolders: YEAR-MONTH / DAY-HOUR / <upload>
|
||||
c rotf=%Y-%m/%d-%H
|
||||
# delete uploads when they are 24 hours old
|
||||
c lifetime=86400
|
||||
# add the parser and tell copyparty what tags it can expect from it
|
||||
c mtp=yt-id,yt-title,yt-author,yt-channel,yt-views,yt-private,yt-manifest,yt-expires=bin/mtag/yt-ipr.py
|
||||
# decide which tags we want to index and in what order
|
||||
c mte=yt-id,yt-title,yt-author,yt-channel,yt-views,yt-private,yt-manifest,yt-expires
|
||||
|
||||
|
||||
# create any other volumes you'd like down here, or merge this with an existing config file
|
||||
48
bin/mtag/res/yt-ipr.user.js
Normal file
48
bin/mtag/res/yt-ipr.user.js
Normal file
@@ -0,0 +1,48 @@
|
||||
// ==UserScript==
|
||||
// @name youtube-playerdata-hub
|
||||
// @match https://youtube.com/*
|
||||
// @match https://*.youtube.com/*
|
||||
// @version 1.0
|
||||
// @grant GM_addStyle
|
||||
// ==/UserScript==
|
||||
|
||||
function main() {
|
||||
var server = 'https://127.0.0.1:3923/ytm?pw=wark',
|
||||
interval = 60; // sec
|
||||
|
||||
var sent = {};
|
||||
function send(txt, mf_url, desc) {
|
||||
if (sent[mf_url])
|
||||
return;
|
||||
|
||||
fetch(server + '&_=' + Date.now(), { method: "PUT", body: txt });
|
||||
console.log('[yt-pdh] yeet %d bytes, %s', txt.length, desc);
|
||||
sent[mf_url] = 1;
|
||||
}
|
||||
|
||||
function collect() {
|
||||
setTimeout(collect, interval * 1000);
|
||||
try {
|
||||
var pd = document.querySelector('ytd-watch-flexy');
|
||||
if (!pd)
|
||||
return console.log('[yt-pdh] no video found');
|
||||
|
||||
pd = pd.playerData;
|
||||
var mu = pd.streamingData.dashManifestUrl || pd.streamingData.hlsManifestUrl;
|
||||
if (!mu || !mu.length)
|
||||
return console.log('[yt-pdh] no manifest found');
|
||||
|
||||
var desc = pd.videoDetails.videoId + ', ' + pd.videoDetails.title;
|
||||
send(JSON.stringify(pd), mu, desc);
|
||||
}
|
||||
catch (ex) {
|
||||
console.log("[yt-pdh]", ex);
|
||||
}
|
||||
}
|
||||
collect();
|
||||
}
|
||||
|
||||
var scr = document.createElement('script');
|
||||
scr.textContent = '(' + main.toString() + ')();';
|
||||
(document.head || document.getElementsByTagName('head')[0]).appendChild(scr);
|
||||
console.log('[yt-pdh] a');
|
||||
198
bin/mtag/yt-ipr.py
Normal file
198
bin/mtag/yt-ipr.py
Normal file
@@ -0,0 +1,198 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import re
|
||||
import os
|
||||
import sys
|
||||
import gzip
|
||||
import json
|
||||
import base64
|
||||
import string
|
||||
import urllib.request
|
||||
from datetime import datetime
|
||||
|
||||
"""
|
||||
youtube initial player response
|
||||
|
||||
it's probably best to use this through a config file; see res/yt-ipr.conf
|
||||
|
||||
but if you want to use plain arguments instead then:
|
||||
-v srv/ytm:ytm:w:rw,ed
|
||||
:c,e2ts:c,e2dsa
|
||||
:c,sz=16k-1m:c,maxn=10,300:c,rotf=%Y-%m/%d-%H
|
||||
:c,mtp=yt-id,yt-title,yt-author,yt-channel,yt-views,yt-private,yt-manifest,yt-expires=bin/mtag/yt-ipr.py
|
||||
:c,mte=yt-id,yt-title,yt-author,yt-channel,yt-views,yt-private,yt-manifest,yt-expires
|
||||
|
||||
see res/yt-ipr.user.js for the example userscript to go with this
|
||||
"""
|
||||
|
||||
|
||||
def main():
|
||||
try:
|
||||
with gzip.open(sys.argv[1], "rt", encoding="utf-8", errors="replace") as f:
|
||||
txt = f.read()
|
||||
except:
|
||||
with open(sys.argv[1], "r", encoding="utf-8", errors="replace") as f:
|
||||
txt = f.read()
|
||||
|
||||
txt = "{" + txt.split("{", 1)[1]
|
||||
|
||||
try:
|
||||
pd = json.loads(txt)
|
||||
except json.decoder.JSONDecodeError as ex:
|
||||
pd = json.loads(txt[: ex.pos])
|
||||
|
||||
# print(json.dumps(pd, indent=2))
|
||||
|
||||
if "videoDetails" in pd:
|
||||
parse_youtube(pd)
|
||||
else:
|
||||
parse_freg(pd)
|
||||
|
||||
|
||||
def get_expiration(url):
|
||||
et = re.search(r"[?&]expire=([0-9]+)", url).group(1)
|
||||
et = datetime.utcfromtimestamp(int(et))
|
||||
return et.strftime("%Y-%m-%d, %H:%M")
|
||||
|
||||
|
||||
def parse_youtube(pd):
|
||||
vd = pd["videoDetails"]
|
||||
sd = pd["streamingData"]
|
||||
|
||||
et = sd["adaptiveFormats"][0]["url"]
|
||||
et = get_expiration(et)
|
||||
|
||||
mf = []
|
||||
if "dashManifestUrl" in sd:
|
||||
mf.append("dash")
|
||||
if "hlsManifestUrl" in sd:
|
||||
mf.append("hls")
|
||||
|
||||
r = {
|
||||
"yt-id": vd["videoId"],
|
||||
"yt-title": vd["title"],
|
||||
"yt-author": vd["author"],
|
||||
"yt-channel": vd["channelId"],
|
||||
"yt-views": vd["viewCount"],
|
||||
"yt-private": vd["isPrivate"],
|
||||
# "yt-expires": sd["expiresInSeconds"],
|
||||
"yt-manifest": ",".join(mf),
|
||||
"yt-expires": et,
|
||||
}
|
||||
print(json.dumps(r))
|
||||
|
||||
freg_conv(pd)
|
||||
|
||||
|
||||
def parse_freg(pd):
|
||||
md = pd["metadata"]
|
||||
r = {
|
||||
"yt-id": md["id"],
|
||||
"yt-title": md["title"],
|
||||
"yt-author": md["channelName"],
|
||||
"yt-channel": md["channelURL"].strip("/").split("/")[-1],
|
||||
"yt-expires": get_expiration(list(pd["video"].values())[0]),
|
||||
}
|
||||
print(json.dumps(r))
|
||||
|
||||
|
||||
def freg_conv(pd):
|
||||
# based on getURLs.js v1.5 (2021-08-07)
|
||||
# fmt: off
|
||||
priority = {
|
||||
"video": [
|
||||
337, 315, 266, 138, # 2160p60
|
||||
313, 336, # 2160p
|
||||
308, # 1440p60
|
||||
271, 264, # 1440p
|
||||
335, 303, 299, # 1080p60
|
||||
248, 169, 137, # 1080p
|
||||
334, 302, 298, # 720p60
|
||||
247, 136 # 720p
|
||||
],
|
||||
"audio": [
|
||||
251, 141, 171, 140, 250, 249, 139
|
||||
]
|
||||
}
|
||||
|
||||
vid_id = pd["videoDetails"]["videoId"]
|
||||
chan_id = pd["videoDetails"]["channelId"]
|
||||
|
||||
try:
|
||||
thumb_url = pd["microformat"]["playerMicroformatRenderer"]["thumbnail"]["thumbnails"][0]["url"]
|
||||
start_ts = pd["microformat"]["playerMicroformatRenderer"]["liveBroadcastDetails"]["startTimestamp"]
|
||||
except:
|
||||
thumb_url = f"https://img.youtube.com/vi/{vid_id}/maxresdefault.jpg"
|
||||
start_ts = ""
|
||||
|
||||
# fmt: on
|
||||
|
||||
metadata = {
|
||||
"title": pd["videoDetails"]["title"],
|
||||
"id": vid_id,
|
||||
"channelName": pd["videoDetails"]["author"],
|
||||
"channelURL": "https://www.youtube.com/channel/" + chan_id,
|
||||
"description": pd["videoDetails"]["shortDescription"],
|
||||
"thumbnailUrl": thumb_url,
|
||||
"startTimestamp": start_ts,
|
||||
}
|
||||
|
||||
if [x for x in vid_id if x not in string.ascii_letters + string.digits + "_-"]:
|
||||
print(f"malicious json", file=sys.stderr)
|
||||
return
|
||||
|
||||
basepath = os.path.dirname(sys.argv[1])
|
||||
|
||||
thumb_fn = f"{basepath}/{vid_id}.jpg"
|
||||
tmp_fn = f"{thumb_fn}.{os.getpid()}"
|
||||
if not os.path.exists(thumb_fn) and (
|
||||
thumb_url.startswith("https://img.youtube.com/vi/")
|
||||
or thumb_url.startswith("https://i.ytimg.com/vi/")
|
||||
):
|
||||
try:
|
||||
with urllib.request.urlopen(thumb_url) as fi:
|
||||
with open(tmp_fn, "wb") as fo:
|
||||
fo.write(fi.read())
|
||||
|
||||
os.rename(tmp_fn, thumb_fn)
|
||||
except:
|
||||
if os.path.exists(tmp_fn):
|
||||
os.unlink(tmp_fn)
|
||||
|
||||
try:
|
||||
with open(thumb_fn, "rb") as f:
|
||||
thumb = base64.b64encode(f.read()).decode("ascii")
|
||||
except:
|
||||
thumb = "/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/yQALCAABAAEBAREA/8wABgAQEAX/2gAIAQEAAD8A0s8g/9k="
|
||||
|
||||
metadata["thumbnail"] = "data:image/jpeg;base64," + thumb
|
||||
|
||||
ret = {
|
||||
"metadata": metadata,
|
||||
"version": "1.5",
|
||||
"createTime": datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||
}
|
||||
|
||||
for stream, itags in priority.items():
|
||||
for itag in itags:
|
||||
url = None
|
||||
for afmt in pd["streamingData"]["adaptiveFormats"]:
|
||||
if itag == afmt["itag"]:
|
||||
url = afmt["url"]
|
||||
break
|
||||
|
||||
if url:
|
||||
ret[stream] = {itag: url}
|
||||
break
|
||||
|
||||
fn = f"{basepath}/{vid_id}.urls.json"
|
||||
with open(fn, "w", encoding="utf-8", errors="replace") as f:
|
||||
f.write(json.dumps(ret, indent=4))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except:
|
||||
# raise
|
||||
pass
|
||||
94
bin/prisonparty.sh
Executable file
94
bin/prisonparty.sh
Executable file
@@ -0,0 +1,94 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# runs copyparty (or any other python script really) in a chroot
|
||||
#
|
||||
# assumption: these directories, and everything within, are owned by root
|
||||
sysdirs=(bin lib lib32 lib64 sbin usr)
|
||||
|
||||
|
||||
# error-handler
|
||||
help() { cat <<'EOF'
|
||||
|
||||
usage:
|
||||
./prisonparty.sh <ROOTDIR> <UID> <GID> [VOLDIR [VOLDIR...]] -- copyparty-sfx.py [...]"
|
||||
|
||||
example:
|
||||
./prisonparty.sh /var/jail 1000 1000 /mnt/nas/music -- copyparty-sfx.py -v /mnt/nas/music::rwmd"
|
||||
|
||||
EOF
|
||||
exit 1
|
||||
}
|
||||
|
||||
|
||||
# read arguments
|
||||
trap help EXIT
|
||||
jail="$1"; shift
|
||||
uid="$1"; shift
|
||||
gid="$1"; shift
|
||||
|
||||
vols=()
|
||||
while true; do
|
||||
v="$1"; shift
|
||||
[ "$v" = -- ] && break # end of volumes
|
||||
[ "$#" -eq 0 ] && break # invalid usage
|
||||
vols+=("$v")
|
||||
done
|
||||
cpp="$1"; shift
|
||||
cpp="$(realpath "$cpp")"
|
||||
cppdir="$(dirname "$cpp")"
|
||||
trap - EXIT
|
||||
|
||||
|
||||
# debug/vis
|
||||
echo
|
||||
echo "chroot-dir = $jail"
|
||||
echo "user:group = $uid:$gid"
|
||||
echo " copyparty = $cpp"
|
||||
echo
|
||||
printf '\033[33m%s\033[0m\n' "copyparty can access these folders and all their subdirectories:"
|
||||
for v in "${vols[@]}"; do
|
||||
printf '\033[36m ├─\033[0m %s \033[36m ── added by (You)\033[0m\n' "$v"
|
||||
done
|
||||
printf '\033[36m ├─\033[0m %s \033[36m ── where the copyparty binary is\033[0m\n' "$cppdir"
|
||||
printf '\033[36m ╰─\033[0m %s \033[36m ── the folder you are currently in\033[0m\n' "$PWD"
|
||||
vols+=("$cppdir" "$PWD")
|
||||
echo
|
||||
|
||||
|
||||
# resolve and remove trailing slash
|
||||
jail="$(realpath "$jail")"
|
||||
jail="${jail%/}"
|
||||
|
||||
|
||||
# bind-mount system directories and volumes
|
||||
printf '%s\n' "${sysdirs[@]}" "${vols[@]}" | LC_ALL=C sort |
|
||||
while IFS= read -r v; do
|
||||
[ -e "/$v" ] || {
|
||||
# printf '\033[1;31mfolder does not exist:\033[0m %s\n' "$v"
|
||||
continue
|
||||
}
|
||||
mkdir -p "$jail/$v"
|
||||
mount | grep -qF " on $jail/$v " ||
|
||||
mount --bind /$v "$jail/$v"
|
||||
done
|
||||
|
||||
|
||||
# create a tmp
|
||||
mkdir -p "$jail/tmp"
|
||||
chown -R "$uid:$gid" "$jail/tmp"
|
||||
|
||||
|
||||
# run copyparty
|
||||
/sbin/chroot --userspec=$uid:$gid "$jail" "$(which python3)" "$cpp" "$@" && rv=0 || rv=$?
|
||||
|
||||
|
||||
# cleanup if not in use
|
||||
lsof "$jail" | grep -qF "$jail" &&
|
||||
echo "chroot is in use, will not cleanup" ||
|
||||
{
|
||||
mount | grep -F " on $jail" |
|
||||
awk '{sub(/ type .*/,"");sub(/.* on /,"");print}' |
|
||||
LC_ALL=C sort -r | tr '\n' '\0' | xargs -r0 umount
|
||||
}
|
||||
exit $rv
|
||||
@@ -25,6 +25,28 @@ ANYWIN = WINDOWS or sys.platform in ["msys"]
|
||||
MACOS = platform.system() == "Darwin"
|
||||
|
||||
|
||||
def get_unix_home():
|
||||
try:
|
||||
v = os.environ["XDG_CONFIG_HOME"]
|
||||
if not v:
|
||||
raise Exception()
|
||||
ret = os.path.normpath(v)
|
||||
os.listdir(ret)
|
||||
return ret
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
v = os.path.expanduser("~/.config")
|
||||
if v.startswith("~"):
|
||||
raise Exception()
|
||||
ret = os.path.normpath(v)
|
||||
os.listdir(ret)
|
||||
return ret
|
||||
except:
|
||||
return "/tmp"
|
||||
|
||||
|
||||
class EnvParams(object):
|
||||
def __init__(self):
|
||||
self.t0 = time.time()
|
||||
@@ -37,10 +59,7 @@ class EnvParams(object):
|
||||
elif sys.platform == "darwin":
|
||||
self.cfg = os.path.expanduser("~/Library/Preferences/copyparty")
|
||||
else:
|
||||
self.cfg = os.path.normpath(
|
||||
os.getenv("XDG_CONFIG_HOME", os.path.expanduser("~/.config"))
|
||||
+ "/copyparty"
|
||||
)
|
||||
self.cfg = get_unix_home() + "/copyparty"
|
||||
|
||||
self.cfg = self.cfg.replace("\\", "/")
|
||||
try:
|
||||
|
||||
@@ -23,7 +23,7 @@ from textwrap import dedent
|
||||
from .__init__ import E, WINDOWS, VT100, PY2, unicode
|
||||
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, ansi_re
|
||||
from .authsrv import re_vol
|
||||
|
||||
HAVE_SSL = True
|
||||
@@ -67,8 +67,12 @@ class Dodge11874(RiceFormatter):
|
||||
def lprint(*a, **ka):
|
||||
global printed
|
||||
|
||||
printed += " ".join(unicode(x) for x in a) + ka.get("end", "\n")
|
||||
print(*a, **ka)
|
||||
txt = " ".join(unicode(x) for x in a) + ka.get("end", "\n")
|
||||
printed += txt
|
||||
if not VT100:
|
||||
txt = ansi_re.sub("", txt)
|
||||
|
||||
print(txt, **ka)
|
||||
|
||||
|
||||
def warn(msg):
|
||||
@@ -197,12 +201,18 @@ def run_argparse(argv, formatter):
|
||||
formatter_class=formatter,
|
||||
prog="copyparty",
|
||||
description="http file sharing hub v{} ({})".format(S_VERSION, S_BUILD_DT),
|
||||
epilog=dedent(
|
||||
"""
|
||||
)
|
||||
|
||||
sects = [
|
||||
[
|
||||
"accounts",
|
||||
"accounts and volumes",
|
||||
dedent(
|
||||
"""
|
||||
-a takes username:password,
|
||||
-v takes src:dst:perm1:perm2:permN:cflag1:cflag2:cflagN:...
|
||||
-v takes src:dst:perm1:perm2:permN:volflag1:volflag2:volflagN:...
|
||||
where "perm" is "accesslevels,username1,username2,..."
|
||||
and "cflag" is config flags to set on this volume
|
||||
and "volflag" is config flags to set on this volume
|
||||
|
||||
list of accesslevels:
|
||||
"r" (read): list folder contents, download files
|
||||
@@ -210,11 +220,7 @@ def run_argparse(argv, formatter):
|
||||
"m" (move): move files and folders; need "w" at destination
|
||||
"d" (delete): permanently delete files and folders
|
||||
|
||||
list of cflags:
|
||||
"c,nodupe" rejects existing files (instead of symlinking them)
|
||||
"c,e2d" sets -e2d (all -e2* args can be set using ce2* cflags)
|
||||
"c,d2t" disables metadata collection, overrides -e2t*
|
||||
"c,d2d" disables all database stuff, overrides -e2*
|
||||
too many volflags to list here, see the other sections
|
||||
|
||||
example:\033[35m
|
||||
-a ed:hunter2 -v .::r:rw,ed -v ../inc:dump:w:rw,ed:c,nodupe \033[36m
|
||||
@@ -231,29 +237,86 @@ def run_argparse(argv, formatter):
|
||||
|
||||
consider the config file for more flexible account/volume management,
|
||||
including dynamic reload at runtime (and being more readable w)
|
||||
"""
|
||||
),
|
||||
],
|
||||
[
|
||||
"flags",
|
||||
"list of volflags",
|
||||
dedent(
|
||||
"""
|
||||
volflags are appended to volume definitions, for example,
|
||||
to create a write-only volume with the \033[33mnodupe\033[0m and \033[32mnosub\033[0m flags:
|
||||
\033[35m-v /mnt/inc:/inc:w\033[33m:c,nodupe\033[32m:c,nosub
|
||||
|
||||
\033[0muploads, general:
|
||||
\033[36mnodupe\033[35m rejects existing files (instead of symlinking them)
|
||||
\033[36mnosub\033[35m forces all uploads into the top folder of the vfs
|
||||
\033[36mgz\033[35m allows server-side gzip of uploads with ?gz (also c,xz)
|
||||
\033[36mpk\033[35m forces server-side compression, optional arg: xz,9
|
||||
|
||||
\033[0mupload rules:
|
||||
\033[36mmaxn=250,600\033[35m max 250 uploads over 15min
|
||||
\033[36mmaxb=1g,300\033[35m max 1 GiB over 5min (suffixes: b, k, m, g)
|
||||
\033[36msz=1k-3m\033[35m allow filesizes between 1 KiB and 3MiB
|
||||
|
||||
\033[0mupload rotation:
|
||||
(moves all uploads into the specified folder structure)
|
||||
\033[36mrotn=100,3\033[35m 3 levels of subfolders with 100 entries in each
|
||||
\033[36mrotf=%Y-%m/%d-%H\033[35m date-formatted organizing
|
||||
\033[36mlifetime=3600\033[35m uploads are deleted after 1 hour
|
||||
|
||||
\033[0mdatabase, general:
|
||||
\033[36me2d\033[35m sets -e2d (all -e2* args can be set using ce2* volflags)
|
||||
\033[36md2t\033[35m disables metadata collection, overrides -e2t*
|
||||
\033[36md2d\033[35m disables all database stuff, overrides -e2*
|
||||
\033[36mdhash\033[35m disables file hashing on initial scans, also ehash
|
||||
\033[36mhist=/tmp/cdb\033[35m puts thumbnails and indexes at that location
|
||||
\033[36mscan=60\033[35m scan for new files every 60sec, same as --re-maxage
|
||||
|
||||
\033[0mdatabase, audio tags:
|
||||
"mte", "mth", "mtp", "mtm" all work the same as -mte, -mth, ...
|
||||
\033[36mmtp=.bpm=f,audio-bpm.py\033[35m uses the "audio-bpm.py" program to
|
||||
generate ".bpm" tags from uploads (f = overwrite tags)
|
||||
\033[36mmtp=ahash,vhash=media-hash.py\033[35m collects two tags at once
|
||||
\033[0m"""
|
||||
),
|
||||
],
|
||||
[
|
||||
"urlform",
|
||||
"",
|
||||
dedent(
|
||||
"""
|
||||
values for --urlform:
|
||||
"stash" dumps the data to file and returns length + checksum
|
||||
"save,get" dumps to file and returns the page like a GET
|
||||
"print,get" prints the data in the log and returns GET
|
||||
\033[36mstash\033[35m dumps the data to file and returns length + checksum
|
||||
\033[36msave,get\033[35m dumps to file and returns the page like a GET
|
||||
\033[36mprint,get\033[35m prints the data in the log and returns GET
|
||||
(leave out the ",get" to return an error instead)
|
||||
|
||||
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)
|
||||
"FLAG" is flags;
|
||||
"v" in addition to realpaths, print usernames and vpaths
|
||||
"ln" only prints symlinks leaving the volume mountpoint
|
||||
"p" exits 1 if any such symlinks are found
|
||||
"r" resumes startup after the listing
|
||||
"""
|
||||
),
|
||||
],
|
||||
[
|
||||
"ls",
|
||||
"volume inspection",
|
||||
dedent(
|
||||
"""
|
||||
\033[35m--ls USR,VOL,FLAGS
|
||||
\033[36mUSR\033[0m is a user to browse as; * is anonymous, ** is all users
|
||||
\033[36mVOL\033[0m is a single volume to scan, default is * (all vols)
|
||||
\033[36mFLAG\033[0m is flags;
|
||||
\033[36mv\033[0m in addition to realpaths, print usernames and vpaths
|
||||
\033[36mln\033[0m only prints symlinks leaving the volume mountpoint
|
||||
\033[36mp\033[0m exits 1 if any such symlinks are found
|
||||
\033[36mr\033[0m resumes startup after the listing
|
||||
examples:
|
||||
--ls '**' # list all files which are possible to read
|
||||
--ls '**,*,ln' # check for dangerous symlinks
|
||||
--ls '**,*,ln,p,r' # check, then start normally if safe
|
||||
\033[0m
|
||||
"""
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
]
|
||||
|
||||
# fmt: off
|
||||
u = unicode
|
||||
ap2 = ap.add_argument_group('general options')
|
||||
@@ -292,6 +355,7 @@ def run_argparse(argv, formatter):
|
||||
ap2.add_argument("-nih", action="store_true", help="no info hostname")
|
||||
ap2.add_argument("-nid", action="store_true", help="no info disk-usage")
|
||||
ap2.add_argument("--no-zip", action="store_true", help="disable download as zip/tar")
|
||||
ap2.add_argument("--no-lifetime", action="store_true", help="disable automatic deletion of uploads after a certain time (lifetime volflag)")
|
||||
|
||||
ap2 = ap.add_argument_group('safety options')
|
||||
ap2.add_argument("--ls", metavar="U[,V[,F]]", type=u, help="scan all volumes; arguments USER,VOL,FLAGS; example [**,*,ln,p,r]")
|
||||
@@ -330,7 +394,7 @@ def run_argparse(argv, formatter):
|
||||
ap2.add_argument("--hist", metavar="PATH", type=u, help="where to store volume data (db, thumbs)")
|
||||
ap2.add_argument("--no-hash", action="store_true", help="disable hashing during e2ds folder scans")
|
||||
ap2.add_argument("--re-int", metavar="SEC", type=int, default=30, help="disk rescan check interval")
|
||||
ap2.add_argument("--re-maxage", metavar="SEC", type=int, default=0, help="disk rescan volume interval (0=off)")
|
||||
ap2.add_argument("--re-maxage", metavar="SEC", type=int, default=0, help="disk rescan volume interval, 0=off, can be set per-volume with the 'scan' volflag")
|
||||
ap2.add_argument("--srch-time", metavar="SEC", type=int, default=30, help="search deadline")
|
||||
|
||||
ap2 = ap.add_argument_group('metadata db options')
|
||||
@@ -357,10 +421,22 @@ def run_argparse(argv, formatter):
|
||||
ap2.add_argument("--no-htp", action="store_true", help="disable httpserver threadpool, create threads as-needed instead")
|
||||
ap2.add_argument("--stackmon", metavar="P,S", type=u, help="write stacktrace to Path every S second")
|
||||
ap2.add_argument("--log-thrs", metavar="SEC", type=float, help="list active threads every SEC")
|
||||
|
||||
return ap.parse_args(args=argv[1:])
|
||||
# fmt: on
|
||||
|
||||
ap2 = ap.add_argument_group("help sections")
|
||||
for k, h, _ in sects:
|
||||
ap2.add_argument("--help-" + k, action="store_true", help=h)
|
||||
|
||||
ret = ap.parse_args(args=argv[1:])
|
||||
for k, h, t in sects:
|
||||
k2 = "help_" + k.replace("-", "_")
|
||||
if vars(ret)[k2]:
|
||||
lprint("# {} help page".format(k))
|
||||
lprint(t + "\033[0m")
|
||||
sys.exit(0)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
time.strptime("19970815", "%Y%m%d") # python#7980
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# coding: utf-8
|
||||
|
||||
VERSION = (0, 12, 12)
|
||||
CODENAME = "fil\033[33med"
|
||||
BUILD_DT = (2021, 8, 6)
|
||||
VERSION = (0, 13, 1)
|
||||
CODENAME = "future-proof"
|
||||
BUILD_DT = (2021, 8, 9)
|
||||
|
||||
S_VERSION = ".".join(map(str, VERSION))
|
||||
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)
|
||||
|
||||
@@ -5,15 +5,29 @@ import re
|
||||
import os
|
||||
import sys
|
||||
import stat
|
||||
import time
|
||||
import base64
|
||||
import hashlib
|
||||
import threading
|
||||
from datetime import datetime
|
||||
|
||||
from .__init__ import WINDOWS
|
||||
from .util import IMPLICATIONS, uncyg, undot, absreal, Pebkac, fsdec, fsenc, statdir
|
||||
from .util import (
|
||||
IMPLICATIONS,
|
||||
uncyg,
|
||||
undot,
|
||||
unhumanize,
|
||||
absreal,
|
||||
Pebkac,
|
||||
fsenc,
|
||||
statdir,
|
||||
)
|
||||
from .bos import bos
|
||||
|
||||
|
||||
LEELOO_DALLAS = "leeloo_dallas"
|
||||
|
||||
|
||||
class AXS(object):
|
||||
def __init__(self, uread=None, uwrite=None, umove=None, udel=None):
|
||||
self.uread = {} if uread is None else {k: 1 for k in uread}
|
||||
@@ -30,6 +44,156 @@ class AXS(object):
|
||||
)
|
||||
|
||||
|
||||
class Lim(object):
|
||||
def __init__(self):
|
||||
self.nups = {} # num tracker
|
||||
self.bups = {} # byte tracker list
|
||||
self.bupc = {} # byte tracker cache
|
||||
|
||||
self.nosub = False # disallow subdirectories
|
||||
|
||||
self.smin = None # filesize min
|
||||
self.smax = None # filesize max
|
||||
|
||||
self.bwin = None # bytes window
|
||||
self.bmax = None # bytes max
|
||||
self.nwin = None # num window
|
||||
self.nmax = None # num max
|
||||
|
||||
self.rotn = None # rot num files
|
||||
self.rotl = None # rot depth
|
||||
self.rotf = None # rot datefmt
|
||||
self.rot_re = None # rotf check
|
||||
|
||||
def set_rotf(self, fmt):
|
||||
self.rotf = fmt
|
||||
r = re.escape(fmt).replace("%Y", "[0-9]{4}").replace("%j", "[0-9]{3}")
|
||||
r = re.sub("%[mdHMSWU]", "[0-9]{2}", r)
|
||||
self.rot_re = re.compile("(^|/)" + r + "$")
|
||||
|
||||
def all(self, ip, rem, sz, abspath):
|
||||
self.chk_nup(ip)
|
||||
self.chk_bup(ip)
|
||||
self.chk_rem(rem)
|
||||
if sz != -1:
|
||||
self.chk_sz(sz)
|
||||
|
||||
ap2, vp2 = self.rot(abspath)
|
||||
if abspath == ap2:
|
||||
return ap2, rem
|
||||
|
||||
return ap2, ("{}/{}".format(rem, vp2) if rem else vp2)
|
||||
|
||||
def chk_sz(self, sz):
|
||||
if self.smin is not None and sz < self.smin:
|
||||
raise Pebkac(400, "file too small")
|
||||
|
||||
if self.smax is not None and sz > self.smax:
|
||||
raise Pebkac(400, "file too big")
|
||||
|
||||
def chk_rem(self, rem):
|
||||
if self.nosub and rem:
|
||||
raise Pebkac(500, "no subdirectories allowed")
|
||||
|
||||
def rot(self, path):
|
||||
if not self.rotf and not self.rotn:
|
||||
return path, ""
|
||||
|
||||
if self.rotf:
|
||||
path = path.rstrip("/\\")
|
||||
if self.rot_re.search(path.replace("\\", "/")):
|
||||
return path, ""
|
||||
|
||||
suf = datetime.utcnow().strftime(self.rotf)
|
||||
if path:
|
||||
path += "/"
|
||||
|
||||
return path + suf, suf
|
||||
|
||||
ret = self.dive(path, self.rotl)
|
||||
if not ret:
|
||||
raise Pebkac(500, "no available slots in volume")
|
||||
|
||||
d = ret[len(path) :].strip("/\\").replace("\\", "/")
|
||||
return ret, d
|
||||
|
||||
def dive(self, path, lvs):
|
||||
items = bos.listdir(path)
|
||||
|
||||
if not lvs:
|
||||
# at leaf level
|
||||
return None if len(items) >= self.rotn else ""
|
||||
|
||||
dirs = [int(x) for x in items if x and all(y in "1234567890" for y in x)]
|
||||
dirs.sort()
|
||||
|
||||
if not dirs:
|
||||
# no branches yet; make one
|
||||
sub = os.path.join(path, "0")
|
||||
bos.mkdir(sub)
|
||||
else:
|
||||
# try newest branch only
|
||||
sub = os.path.join(path, str(dirs[-1]))
|
||||
|
||||
ret = self.dive(sub, lvs - 1)
|
||||
if ret is not None:
|
||||
return os.path.join(sub, ret)
|
||||
|
||||
if len(dirs) >= self.rotn:
|
||||
# full branch or root
|
||||
return None
|
||||
|
||||
# make a branch
|
||||
sub = os.path.join(path, str(dirs[-1] + 1))
|
||||
bos.mkdir(sub)
|
||||
ret = self.dive(sub, lvs - 1)
|
||||
if ret is None:
|
||||
raise Pebkac(500, "rotation bug")
|
||||
|
||||
return os.path.join(sub, ret)
|
||||
|
||||
def nup(self, ip):
|
||||
try:
|
||||
self.nups[ip].append(time.time())
|
||||
except:
|
||||
self.nups[ip] = [time.time()]
|
||||
|
||||
def bup(self, ip, nbytes):
|
||||
v = [time.time(), nbytes]
|
||||
try:
|
||||
self.bups[ip].append(v)
|
||||
self.bupc[ip] += nbytes
|
||||
except:
|
||||
self.bups[ip] = [v]
|
||||
self.bupc[ip] = nbytes
|
||||
|
||||
def chk_nup(self, ip):
|
||||
if not self.nmax or ip not in self.nups:
|
||||
return
|
||||
|
||||
nups = self.nups[ip]
|
||||
cutoff = time.time() - self.nwin
|
||||
while nups and nups[0] < cutoff:
|
||||
nups.pop(0)
|
||||
|
||||
if len(nups) >= self.nmax:
|
||||
raise Pebkac(429, "too many uploads")
|
||||
|
||||
def chk_bup(self, ip):
|
||||
if not self.bmax or ip not in self.bups:
|
||||
return
|
||||
|
||||
bups = self.bups[ip]
|
||||
cutoff = time.time() - self.bwin
|
||||
mark = self.bupc[ip]
|
||||
while bups and bups[0][0] < cutoff:
|
||||
mark -= bups.pop(0)[1]
|
||||
|
||||
self.bupc[ip] = mark
|
||||
if mark >= self.bmax:
|
||||
raise Pebkac(429, "ingress saturated")
|
||||
|
||||
|
||||
class VFS(object):
|
||||
"""single level in the virtual fs"""
|
||||
|
||||
@@ -42,6 +206,7 @@ class VFS(object):
|
||||
self.nodes = {} # child nodes
|
||||
self.histtab = None # all realpath->histpath
|
||||
self.dbv = None # closest full/non-jump parent
|
||||
self.lim = None # type: Lim # upload limits; only set for dbv
|
||||
|
||||
if realpath:
|
||||
self.histpath = os.path.join(realpath, ".hist") # db / thumbcache
|
||||
@@ -165,13 +330,14 @@ class VFS(object):
|
||||
[will_move, c.umove, "move"],
|
||||
[will_del, c.udel, "delete"],
|
||||
]:
|
||||
if req and (uname not in d and "*" not in d):
|
||||
if req and (uname not in d and "*" not in d) and uname != LEELOO_DALLAS:
|
||||
m = "you don't have {}-access for this location"
|
||||
raise Pebkac(403, m.format(msg))
|
||||
|
||||
return vn, rem
|
||||
|
||||
def get_dbv(self, vrem):
|
||||
# type: (str) -> tuple[VFS, str]
|
||||
dbv = self.dbv
|
||||
if not dbv:
|
||||
return self, vrem
|
||||
@@ -391,6 +557,9 @@ class AuthSrv(object):
|
||||
|
||||
def _read_vol_str(self, lvl, uname, axs, flags):
|
||||
# type: (str, str, AXS, any) -> None
|
||||
if lvl.strip("crwmd"):
|
||||
raise Exception("invalid volume flag: {},{}".format(lvl, uname))
|
||||
|
||||
if lvl == "c":
|
||||
cval = True
|
||||
if "=" in uname:
|
||||
@@ -546,6 +715,9 @@ class AuthSrv(object):
|
||||
)
|
||||
raise Exception("invalid config")
|
||||
|
||||
if LEELOO_DALLAS in all_users:
|
||||
raise Exception("sorry, reserved username: " + LEELOO_DALLAS)
|
||||
|
||||
promote = []
|
||||
demote = []
|
||||
for vol in vfs.all_vols.values():
|
||||
@@ -604,6 +776,51 @@ class AuthSrv(object):
|
||||
|
||||
vfs.histtab = {v.realpath: v.histpath for v in vfs.all_vols.values()}
|
||||
|
||||
for vol in vfs.all_vols.values():
|
||||
lim = Lim()
|
||||
use = False
|
||||
|
||||
if vol.flags.get("nosub"):
|
||||
use = True
|
||||
lim.nosub = True
|
||||
|
||||
v = vol.flags.get("sz")
|
||||
if v:
|
||||
use = True
|
||||
lim.smin, lim.smax = [unhumanize(x) for x in v.split("-")]
|
||||
|
||||
v = vol.flags.get("rotn")
|
||||
if v:
|
||||
use = True
|
||||
lim.rotn, lim.rotl = [int(x) for x in v.split(",")]
|
||||
|
||||
v = vol.flags.get("rotf")
|
||||
if v:
|
||||
use = True
|
||||
lim.set_rotf(v)
|
||||
|
||||
v = vol.flags.get("maxn")
|
||||
if v:
|
||||
use = True
|
||||
lim.nmax, lim.nwin = [int(x) for x in v.split(",")]
|
||||
|
||||
v = vol.flags.get("maxb")
|
||||
if v:
|
||||
use = True
|
||||
lim.bmax, lim.bwin = [unhumanize(x) for x in v.split(",")]
|
||||
|
||||
if use:
|
||||
vol.lim = lim
|
||||
|
||||
for vol in vfs.all_vols.values():
|
||||
if "pk" in vol.flags and "gz" not in vol.flags and "xz" not in vol.flags:
|
||||
vol.flags["gz"] = False # def.pk
|
||||
|
||||
if "scan" in vol.flags:
|
||||
vol.flags["scan"] = int(vol.flags["scan"])
|
||||
elif self.args.re_maxage:
|
||||
vol.flags["scan"] = self.args.re_maxage
|
||||
|
||||
all_mte = {}
|
||||
errors = False
|
||||
for vol in vfs.all_vols.values():
|
||||
|
||||
@@ -13,10 +13,15 @@ import ctypes
|
||||
from datetime import datetime
|
||||
import calendar
|
||||
|
||||
try:
|
||||
import lzma
|
||||
except:
|
||||
pass
|
||||
|
||||
from .__init__ import E, PY2, WINDOWS, ANYWIN, unicode
|
||||
from .util import * # noqa # pylint: disable=unused-wildcard-import
|
||||
from .bos import bos
|
||||
from .authsrv import AuthSrv
|
||||
from .authsrv import AuthSrv, Lim
|
||||
from .szip import StreamZip
|
||||
from .star import StreamTar
|
||||
|
||||
@@ -107,7 +112,7 @@ class HttpCli(object):
|
||||
self.http_ver = "HTTP/1.1"
|
||||
# self.log("pebkac at httpcli.run #1: " + repr(ex))
|
||||
self.keepalive = False
|
||||
self.loud_reply(unicode(ex), status=ex.code)
|
||||
self.loud_reply(unicode(ex), status=ex.code, volsan=True)
|
||||
return self.keepalive
|
||||
|
||||
# time.sleep(0.4)
|
||||
@@ -180,6 +185,9 @@ class HttpCli(object):
|
||||
if kc in cookies and ku not in uparam:
|
||||
uparam[ku] = cookies[kc]
|
||||
|
||||
if len(uparam) > 10 or len(cookies) > 50:
|
||||
raise Pebkac(400, "u wot m8")
|
||||
|
||||
self.uparam = uparam
|
||||
self.cookies = cookies
|
||||
self.vpath = unquotep(vpath) # not query, so + means +
|
||||
@@ -216,19 +224,24 @@ class HttpCli(object):
|
||||
else:
|
||||
raise Pebkac(400, 'invalid HTTP mode "{0}"'.format(self.mode))
|
||||
|
||||
except Pebkac as ex:
|
||||
except Exception as ex:
|
||||
pex = ex
|
||||
if not hasattr(ex, "code"):
|
||||
pex = Pebkac(500)
|
||||
|
||||
try:
|
||||
# self.log("pebkac at httpcli.run #2: " + repr(ex))
|
||||
post = self.mode in ["POST", "PUT"] or "content-length" in self.headers
|
||||
if not self._check_nonfatal(ex, post):
|
||||
if not self._check_nonfatal(pex, post):
|
||||
self.keepalive = False
|
||||
|
||||
self.log("{}\033[0m, {}".format(str(ex), self.vpath), 3)
|
||||
msg = str(ex) if pex == ex else min_ex()
|
||||
self.log("{}\033[0m, {}".format(msg, self.vpath), 3)
|
||||
|
||||
msg = "<pre>{}\r\nURL: {}\r\n".format(str(ex), self.vpath)
|
||||
if self.hint:
|
||||
msg += "hint: {}\r\n".format(self.hint)
|
||||
|
||||
self.reply(msg.encode("utf-8", "replace"), status=ex.code)
|
||||
self.reply(msg.encode("utf-8", "replace"), status=pex.code, volsan=True)
|
||||
return self.keepalive
|
||||
except Pebkac:
|
||||
return False
|
||||
@@ -261,8 +274,12 @@ class HttpCli(object):
|
||||
except:
|
||||
raise Pebkac(400, "client d/c while replying headers")
|
||||
|
||||
def reply(self, body, status=200, mime=None, headers=None):
|
||||
def reply(self, body, status=200, mime=None, headers=None, volsan=False):
|
||||
# TODO something to reply with user-supplied values safely
|
||||
|
||||
if volsan:
|
||||
body = vol_san(self.asrv.vfs.all_vols.values(), body)
|
||||
|
||||
self.send_headers(len(body), status, mime, headers)
|
||||
|
||||
try:
|
||||
@@ -491,7 +508,11 @@ class HttpCli(object):
|
||||
def dump_to_file(self):
|
||||
reader, remains = self.get_body_reader()
|
||||
vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True)
|
||||
lim = vfs.get_dbv(rem)[0].lim
|
||||
fdir = os.path.join(vfs.realpath, rem)
|
||||
if lim:
|
||||
fdir, rem = lim.all(self.ip, rem, remains, fdir)
|
||||
bos.makedirs(fdir)
|
||||
|
||||
addr = self.ip.replace(":", ".")
|
||||
fn = "put-{:.6f}-{}.bin".format(time.time(), addr)
|
||||
@@ -499,9 +520,70 @@ class HttpCli(object):
|
||||
if self.args.nw:
|
||||
path = os.devnull
|
||||
|
||||
with open(fsenc(path), "wb", 512 * 1024) as f:
|
||||
open_f = open
|
||||
open_a = [fsenc(path), "wb", 512 * 1024]
|
||||
open_ka = {}
|
||||
|
||||
# user-request || config-force
|
||||
if ("gz" in vfs.flags or "xz" in vfs.flags) and (
|
||||
"pk" in vfs.flags
|
||||
or "pk" in self.uparam
|
||||
or "gz" in self.uparam
|
||||
or "xz" in self.uparam
|
||||
):
|
||||
fb = {"gz": 9, "xz": 0} # default/fallback level
|
||||
lv = {} # selected level
|
||||
alg = None # selected algo (gz=preferred)
|
||||
|
||||
# user-prefs first
|
||||
if "gz" in self.uparam or "pk" in self.uparam: # def.pk
|
||||
alg = "gz"
|
||||
if "xz" in self.uparam:
|
||||
alg = "xz"
|
||||
if alg:
|
||||
v = self.uparam.get(alg)
|
||||
lv[alg] = fb[alg] if v is None else int(v)
|
||||
|
||||
if alg not in vfs.flags:
|
||||
alg = "gz" if "gz" in vfs.flags else "xz"
|
||||
|
||||
# then server overrides
|
||||
pk = vfs.flags.get("pk")
|
||||
if pk is not None:
|
||||
# config-forced on
|
||||
alg = alg or "gz" # def.pk
|
||||
try:
|
||||
# config-forced opts
|
||||
alg, lv = pk.split(",")
|
||||
lv[alg] = int(lv)
|
||||
except:
|
||||
pass
|
||||
|
||||
lv[alg] = lv.get(alg) or fb.get(alg)
|
||||
|
||||
self.log("compressing with {} level {}".format(alg, lv.get(alg)))
|
||||
if alg == "gz":
|
||||
open_f = gzip.GzipFile
|
||||
open_a = [fsenc(path), "wb", lv[alg], None, 0x5FEE6600] # 2021-01-01
|
||||
elif alg == "xz":
|
||||
open_f = lzma.open
|
||||
open_a = [fsenc(path), "wb"]
|
||||
open_ka = {"preset": lv[alg]}
|
||||
else:
|
||||
self.log("fallthrough? thats a bug", 1)
|
||||
|
||||
with open_f(*open_a, **open_ka) as f:
|
||||
post_sz, _, sha_b64 = hashcopy(reader, f)
|
||||
|
||||
if lim:
|
||||
lim.nup(self.ip)
|
||||
lim.bup(self.ip, post_sz)
|
||||
try:
|
||||
lim.chk_sz(post_sz)
|
||||
except:
|
||||
bos.unlink(path)
|
||||
raise
|
||||
|
||||
if not self.args.nw:
|
||||
vfs, vrem = vfs.get_dbv(rem)
|
||||
self.conn.hsrv.broker.put(
|
||||
@@ -583,7 +665,7 @@ class HttpCli(object):
|
||||
try:
|
||||
remains = int(self.headers["content-length"])
|
||||
except:
|
||||
raise Pebkac(400, "you must supply a content-length for JSON POST")
|
||||
raise Pebkac(411)
|
||||
|
||||
if remains > 1024 * 1024:
|
||||
raise Pebkac(413, "json 2big")
|
||||
@@ -614,12 +696,9 @@ class HttpCli(object):
|
||||
if self.vpath.endswith(k):
|
||||
self.vpath = self.vpath[: -len(k)]
|
||||
|
||||
sub = None
|
||||
name = undot(body["name"])
|
||||
if "/" in name:
|
||||
sub, name = name.rsplit("/", 1)
|
||||
self.vpath = "/".join([self.vpath, sub]).strip("/")
|
||||
body["name"] = name
|
||||
raise Pebkac(400, "your client is old; press CTRL-SHIFT-R and try again")
|
||||
|
||||
vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True)
|
||||
dbv, vrem = vfs.get_dbv(rem)
|
||||
@@ -630,7 +709,7 @@ class HttpCli(object):
|
||||
body["addr"] = self.ip
|
||||
body["vcfg"] = dbv.flags
|
||||
|
||||
if sub:
|
||||
if rem:
|
||||
try:
|
||||
dst = os.path.join(vfs.realpath, rem)
|
||||
if not bos.path.isdir(dst):
|
||||
@@ -650,9 +729,6 @@ class HttpCli(object):
|
||||
|
||||
x = self.conn.hsrv.broker.put(True, "up2k.handle_json", body)
|
||||
ret = x.get()
|
||||
if sub:
|
||||
ret["name"] = "/".join([sub, ret["name"]])
|
||||
|
||||
ret = json.dumps(ret)
|
||||
self.log(ret)
|
||||
self.reply(ret.encode("utf-8"), mime="application/json")
|
||||
@@ -880,6 +956,11 @@ class HttpCli(object):
|
||||
vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True)
|
||||
self._assert_safe_rem(rem)
|
||||
|
||||
lim = vfs.get_dbv(rem)[0].lim
|
||||
fdir_base = os.path.join(vfs.realpath, rem)
|
||||
if lim:
|
||||
fdir_base, rem = lim.all(self.ip, rem, -1, fdir_base)
|
||||
|
||||
files = []
|
||||
errmsg = ""
|
||||
t0 = time.time()
|
||||
@@ -889,12 +970,9 @@ class HttpCli(object):
|
||||
self.log("discarding incoming file without filename")
|
||||
# fallthrough
|
||||
|
||||
fdir = fdir_base
|
||||
fname = sanitize_fn(p_file, "", [".prologue.html", ".epilogue.html"])
|
||||
if p_file and not nullwrite:
|
||||
fdir = os.path.join(vfs.realpath, rem)
|
||||
fname = sanitize_fn(
|
||||
p_file, "", [".prologue.html", ".epilogue.html"]
|
||||
)
|
||||
|
||||
if not bos.path.isdir(fdir):
|
||||
raise Pebkac(404, "that folder does not exist")
|
||||
|
||||
@@ -905,27 +983,43 @@ class HttpCli(object):
|
||||
fname = os.devnull
|
||||
fdir = ""
|
||||
|
||||
if lim:
|
||||
lim.chk_bup(self.ip)
|
||||
lim.chk_nup(self.ip)
|
||||
if not nullwrite:
|
||||
bos.makedirs(fdir)
|
||||
|
||||
try:
|
||||
with ren_open(fname, "wb", 512 * 1024, **open_args) as f:
|
||||
f, fname = f["orz"]
|
||||
self.log("writing to {}/{}".format(fdir, fname))
|
||||
abspath = os.path.join(fdir, fname)
|
||||
self.log("writing to {}".format(abspath))
|
||||
sz, sha512_hex, _ = hashcopy(p_data, f)
|
||||
if sz == 0:
|
||||
raise Pebkac(400, "empty files in post")
|
||||
|
||||
files.append([sz, sha512_hex, p_file, fname])
|
||||
dbv, vrem = vfs.get_dbv(rem)
|
||||
self.conn.hsrv.broker.put(
|
||||
False,
|
||||
"up2k.hash_file",
|
||||
dbv.realpath,
|
||||
dbv.flags,
|
||||
vrem,
|
||||
fname,
|
||||
self.ip,
|
||||
time.time(),
|
||||
)
|
||||
self.conn.nbyte += sz
|
||||
if lim:
|
||||
lim.nup(self.ip)
|
||||
lim.bup(self.ip, sz)
|
||||
try:
|
||||
lim.chk_sz(sz)
|
||||
except:
|
||||
bos.unlink(abspath)
|
||||
raise
|
||||
|
||||
files.append([sz, sha512_hex, p_file, fname])
|
||||
dbv, vrem = vfs.get_dbv(rem)
|
||||
self.conn.hsrv.broker.put(
|
||||
False,
|
||||
"up2k.hash_file",
|
||||
dbv.realpath,
|
||||
dbv.flags,
|
||||
vrem,
|
||||
fname,
|
||||
self.ip,
|
||||
time.time(),
|
||||
)
|
||||
self.conn.nbyte += sz
|
||||
|
||||
except Pebkac:
|
||||
if fname != os.devnull:
|
||||
@@ -944,7 +1038,7 @@ class HttpCli(object):
|
||||
raise
|
||||
|
||||
except Pebkac as ex:
|
||||
errmsg = unicode(ex)
|
||||
errmsg = volsan(self.asrv.vfs.all_vols.values(), unicode(ex))
|
||||
|
||||
td = max(0.1, time.time() - t0)
|
||||
sz_total = sum(x[0] for x in files)
|
||||
@@ -1023,6 +1117,20 @@ class HttpCli(object):
|
||||
vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True)
|
||||
self._assert_safe_rem(rem)
|
||||
|
||||
clen = int(self.headers.get("content-length", -1))
|
||||
if clen == -1:
|
||||
raise Pebkac(411)
|
||||
|
||||
rp, fn = vsplit(rem)
|
||||
fp = os.path.join(vfs.realpath, rp)
|
||||
lim = vfs.get_dbv(rem)[0].lim
|
||||
if lim:
|
||||
fp, rp = lim.all(self.ip, rp, clen, fp)
|
||||
bos.makedirs(fp)
|
||||
|
||||
fp = os.path.join(fp, fn)
|
||||
rem = "{}/{}".format(rp, fn).strip("/")
|
||||
|
||||
if not rem.endswith(".md"):
|
||||
raise Pebkac(400, "only markdown pls")
|
||||
|
||||
@@ -1034,7 +1142,6 @@ class HttpCli(object):
|
||||
self.reply(response.encode("utf-8"))
|
||||
return True
|
||||
|
||||
fp = os.path.join(vfs.realpath, rem)
|
||||
srv_lastmod = srv_lastmod3 = -1
|
||||
try:
|
||||
st = bos.stat(fp)
|
||||
@@ -1088,6 +1195,15 @@ class HttpCli(object):
|
||||
with open(fsenc(fp), "wb", 512 * 1024) as f:
|
||||
sz, sha512, _ = hashcopy(p_data, f)
|
||||
|
||||
if lim:
|
||||
lim.nup(self.ip)
|
||||
lim.bup(self.ip, sz)
|
||||
try:
|
||||
lim.chk_sz(sz)
|
||||
except:
|
||||
bos.unlink(fp)
|
||||
raise
|
||||
|
||||
new_lastmod = bos.stat(fp).st_mtime
|
||||
new_lastmod3 = int(new_lastmod * 1000)
|
||||
sha512 = sha512[:56]
|
||||
|
||||
@@ -18,8 +18,7 @@ def errdesc(errors):
|
||||
tf_path = tf.name
|
||||
tf.write("\r\n".join(report).encode("utf-8", "replace"))
|
||||
|
||||
dt = datetime.utcfromtimestamp(time.time())
|
||||
dt = dt.strftime("%Y-%m%d-%H%M%S")
|
||||
dt = datetime.utcnow().strftime("%Y-%m%d-%H%M%S")
|
||||
|
||||
bos.chmod(tf_path, 0o444)
|
||||
return {
|
||||
|
||||
@@ -14,7 +14,7 @@ from datetime import datetime, timedelta
|
||||
import calendar
|
||||
|
||||
from .__init__ import E, PY2, WINDOWS, ANYWIN, MACOS, VT100, unicode
|
||||
from .util import mp, start_log_thrs, start_stackmon, min_ex
|
||||
from .util import mp, start_log_thrs, start_stackmon, min_ex, ansi_re
|
||||
from .authsrv import AuthSrv
|
||||
from .tcpsrv import TcpSrv
|
||||
from .up2k import Up2k
|
||||
@@ -41,7 +41,6 @@ class SvcHub(object):
|
||||
self.stop_cond = threading.Condition()
|
||||
self.httpsrv_up = 0
|
||||
|
||||
self.ansi_re = re.compile("\033\\[[^m]*m")
|
||||
self.log_mutex = threading.Lock()
|
||||
self.next_day = 0
|
||||
|
||||
@@ -111,7 +110,7 @@ class SvcHub(object):
|
||||
thr.start()
|
||||
|
||||
def _logname(self):
|
||||
dt = datetime.utcfromtimestamp(time.time())
|
||||
dt = datetime.utcnow()
|
||||
fn = self.args.lo
|
||||
for fs in "YmdHMS":
|
||||
fs = "%" + fs
|
||||
@@ -244,8 +243,7 @@ class SvcHub(object):
|
||||
return
|
||||
|
||||
with self.log_mutex:
|
||||
ts = datetime.utcfromtimestamp(time.time())
|
||||
ts = ts.strftime("%Y-%m%d-%H%M%S.%f")[:-3]
|
||||
ts = datetime.utcnow().strftime("%Y-%m%d-%H%M%S.%f")[:-3]
|
||||
self.logf.write("@{} [{}] {}\n".format(ts, src, msg))
|
||||
|
||||
now = time.time()
|
||||
@@ -257,7 +255,7 @@ class SvcHub(object):
|
||||
self.logf.close()
|
||||
self._setup_logfile("")
|
||||
|
||||
dt = datetime.utcfromtimestamp(time.time())
|
||||
dt = datetime.utcnow()
|
||||
|
||||
# unix timestamp of next 00:00:00 (leap-seconds safe)
|
||||
day_now = dt.day
|
||||
@@ -280,9 +278,9 @@ class SvcHub(object):
|
||||
if not VT100:
|
||||
fmt = "{} {:21} {}\n"
|
||||
if "\033" in msg:
|
||||
msg = self.ansi_re.sub("", msg)
|
||||
msg = ansi_re.sub("", msg)
|
||||
if "\033" in src:
|
||||
src = self.ansi_re.sub("", src)
|
||||
src = ansi_re.sub("", src)
|
||||
elif c:
|
||||
if isinstance(c, int):
|
||||
msg = "\033[3{}m{}".format(c, msg)
|
||||
|
||||
@@ -88,7 +88,7 @@ class U2idx(object):
|
||||
is_date = False
|
||||
kw_key = ["(", ")", "and ", "or ", "not "]
|
||||
kw_val = ["==", "=", "!=", ">", ">=", "<", "<=", "like "]
|
||||
ptn_mt = re.compile(r"^\.?[a-z]+$")
|
||||
ptn_mt = re.compile(r"^\.?[a-z_-]+$")
|
||||
mt_ctr = 0
|
||||
mt_keycmp = "substr(up.w,1,16)"
|
||||
mt_keycmp2 = None
|
||||
|
||||
@@ -36,7 +36,7 @@ from .util import (
|
||||
min_ex,
|
||||
)
|
||||
from .bos import bos
|
||||
from .authsrv import AuthSrv
|
||||
from .authsrv import AuthSrv, LEELOO_DALLAS
|
||||
from .mtag import MTag, MParser
|
||||
|
||||
try:
|
||||
@@ -176,36 +176,71 @@ class Up2k(object):
|
||||
return None
|
||||
|
||||
def _sched_rescan(self):
|
||||
maxage = self.args.re_maxage
|
||||
volage = {}
|
||||
while True:
|
||||
time.sleep(self.args.re_int)
|
||||
now = time.time()
|
||||
vpaths = list(sorted(self.asrv.vfs.all_vols.keys()))
|
||||
with self.mutex:
|
||||
if maxage:
|
||||
for vp in vpaths:
|
||||
if vp not in volage:
|
||||
volage[vp] = now
|
||||
for vp, vol in sorted(self.asrv.vfs.all_vols.items()):
|
||||
maxage = vol.flags.get("scan")
|
||||
if not maxage:
|
||||
continue
|
||||
|
||||
if now - volage[vp] >= maxage:
|
||||
self.need_rescan[vp] = 1
|
||||
if vp not in volage:
|
||||
volage[vp] = now
|
||||
|
||||
if not self.need_rescan:
|
||||
continue
|
||||
if now - volage[vp] >= maxage:
|
||||
self.need_rescan[vp] = 1
|
||||
|
||||
vols = list(sorted(self.need_rescan.keys()))
|
||||
self.need_rescan = {}
|
||||
|
||||
err = self.rescan(self.asrv.vfs.all_vols, vols)
|
||||
if err:
|
||||
for v in vols:
|
||||
self.need_rescan[v] = True
|
||||
if vols:
|
||||
err = self.rescan(self.asrv.vfs.all_vols, vols)
|
||||
if err:
|
||||
for v in vols:
|
||||
self.need_rescan[v] = True
|
||||
|
||||
continue
|
||||
|
||||
for v in vols:
|
||||
volage[v] = now
|
||||
|
||||
if self.args.no_lifetime:
|
||||
continue
|
||||
|
||||
for v in vols:
|
||||
volage[v] = now
|
||||
for vp, vol in sorted(self.asrv.vfs.all_vols.items()):
|
||||
lifetime = vol.flags.get("lifetime")
|
||||
if not lifetime:
|
||||
continue
|
||||
|
||||
cur = self.cur.get(vol.realpath)
|
||||
if not cur:
|
||||
continue
|
||||
|
||||
nrm = 0
|
||||
deadline = time.time() - int(lifetime)
|
||||
q = "select rd, fn from up where at > 0 and at < ? limit 100"
|
||||
while True:
|
||||
with self.mutex:
|
||||
hits = cur.execute(q, (deadline,)).fetchall()
|
||||
|
||||
if not hits:
|
||||
break
|
||||
|
||||
for rd, fn in hits:
|
||||
if rd.startswith("//") or fn.startswith("//"):
|
||||
rd, fn = s3dec(rd, fn)
|
||||
|
||||
fvp = "{}/{}".format(rd, fn).strip("/")
|
||||
if vp:
|
||||
fvp = "{}/{}".format(vp, fvp)
|
||||
|
||||
self._handle_rm(LEELOO_DALLAS, None, fvp)
|
||||
nrm += 1
|
||||
|
||||
if nrm:
|
||||
self.log("{} files graduated in {}".format(nrm, vp))
|
||||
|
||||
def _vis_job_progress(self, job):
|
||||
perc = 100 - (len(job["need"]) * 100.0 / len(job["hash"]))
|
||||
@@ -1148,6 +1183,16 @@ class Up2k(object):
|
||||
cur.connection.commit()
|
||||
|
||||
if not job:
|
||||
vfs = self.asrv.vfs.all_vols[cj["vtop"]]
|
||||
if vfs.lim:
|
||||
ap1 = os.path.join(cj["ptop"], cj["prel"])
|
||||
ap2, cj["prel"] = vfs.lim.all(
|
||||
cj["addr"], cj["prel"], cj["size"], ap1
|
||||
)
|
||||
bos.makedirs(ap2)
|
||||
vfs.lim.nup(cj["addr"])
|
||||
vfs.lim.bup(cj["addr"], cj["size"])
|
||||
|
||||
job = {
|
||||
"wark": wark,
|
||||
"t0": now,
|
||||
@@ -1178,8 +1223,11 @@ class Up2k(object):
|
||||
|
||||
self._new_upload(job)
|
||||
|
||||
purl = "/{}/".format("{}/{}".format(job["vtop"], job["prel"]).strip("/"))
|
||||
|
||||
return {
|
||||
"name": job["name"],
|
||||
"purl": purl,
|
||||
"size": job["size"],
|
||||
"lmod": job["lmod"],
|
||||
"hash": job["need"],
|
||||
@@ -1230,7 +1278,7 @@ class Up2k(object):
|
||||
hops = len(ndst[nc:]) - 1
|
||||
lsrc = "../" * hops + "/".join(lsrc)
|
||||
os.symlink(fsenc(lsrc), fsenc(ldst))
|
||||
except (AttributeError, OSError) as ex:
|
||||
except Exception as ex:
|
||||
self.log("cannot symlink; creating copy: " + repr(ex))
|
||||
shutil.copy2(fsenc(src), fsenc(dst))
|
||||
|
||||
|
||||
@@ -58,6 +58,9 @@ except:
|
||||
return struct.unpack(f.decode("ascii"), *a, **ka)
|
||||
|
||||
|
||||
ansi_re = re.compile("\033\\[[^m]*m")
|
||||
|
||||
|
||||
surrogateescape.register_surrogateescape()
|
||||
FS_ENCODING = sys.getfilesystemencoding()
|
||||
if WINDOWS and PY2:
|
||||
@@ -77,6 +80,7 @@ HTTPCODE = {
|
||||
403: "Forbidden",
|
||||
404: "Not Found",
|
||||
405: "Method Not Allowed",
|
||||
411: "Length Required",
|
||||
413: "Payload Too Large",
|
||||
416: "Requested Range Not Satisfiable",
|
||||
422: "Unprocessable Entity",
|
||||
@@ -340,6 +344,15 @@ def log_thrs(log, ival, name):
|
||||
log(name, "\033[0m \033[33m".join(tv), 3)
|
||||
|
||||
|
||||
def vol_san(vols, txt):
|
||||
for vol in vols:
|
||||
txt = txt.replace(
|
||||
vol.realpath.encode("utf-8"), vol.vpath.encode("utf-8")
|
||||
)
|
||||
|
||||
return txt
|
||||
|
||||
|
||||
def min_ex():
|
||||
et, ev, tb = sys.exc_info()
|
||||
tb = traceback.extract_tb(tb)
|
||||
@@ -684,6 +697,17 @@ def humansize(sz, terse=False):
|
||||
return ret.replace("iB", "").replace(" ", "")
|
||||
|
||||
|
||||
def unhumanize(sz):
|
||||
try:
|
||||
return float(sz)
|
||||
except:
|
||||
pass
|
||||
|
||||
mul = sz[-1:].lower()
|
||||
mul = {"k": 1024, "m": 1024 * 1024, "g": 1024 * 1024 * 1024}.get(mul, 1)
|
||||
return float(sz[:-1]) * mul
|
||||
|
||||
|
||||
def get_spd(nbyte, t0, t=None):
|
||||
if t is None:
|
||||
t = time.time()
|
||||
@@ -1065,7 +1089,7 @@ def statdir(logger, scandir, lstat, top):
|
||||
def rmdirs(logger, scandir, lstat, top):
|
||||
if not os.path.exists(fsenc(top)) or not os.path.isdir(fsenc(top)):
|
||||
top = os.path.dirname(top)
|
||||
|
||||
|
||||
dirs = statdir(logger, scandir, lstat, top)
|
||||
dirs = [x[0] for x in dirs if stat.S_ISDIR(x[1].st_mode)]
|
||||
dirs = [os.path.join(top, x) for x in dirs]
|
||||
|
||||
@@ -25,122 +25,6 @@ html, body {
|
||||
pre, code, tt {
|
||||
font-family: monospace, monospace;
|
||||
}
|
||||
|
||||
|
||||
|
||||
#tt, #toast {
|
||||
position: fixed;
|
||||
max-width: 34em;
|
||||
background: #222;
|
||||
border: 0 solid #777;
|
||||
box-shadow: 0 .2em .5em #222;
|
||||
border-radius: .4em;
|
||||
z-index: 9001;
|
||||
}
|
||||
#tt {
|
||||
overflow: hidden;
|
||||
margin-top: 1em;
|
||||
padding: 0 1.3em;
|
||||
height: 0;
|
||||
opacity: .1;
|
||||
transition: opacity 0.14s, height 0.14s, padding 0.14s;
|
||||
}
|
||||
#toast {
|
||||
bottom: 5em;
|
||||
right: -1em;
|
||||
line-height: 1.5em;
|
||||
padding: 1em 1.3em;
|
||||
border-width: .4em 0;
|
||||
transform: translateX(100%);
|
||||
transition:
|
||||
transform .4s cubic-bezier(.2, 1.2, .5, 1),
|
||||
right .4s cubic-bezier(.2, 1.2, .5, 1);
|
||||
text-shadow: 1px 1px 0 #000;
|
||||
color: #fff;
|
||||
}
|
||||
#toastc {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
left: 0;
|
||||
width: 0;
|
||||
opacity: 0;
|
||||
padding: .3em 0;
|
||||
margin: -.3em 0 0 0;
|
||||
line-height: 1.5em;
|
||||
color: #000;
|
||||
border: none;
|
||||
outline: none;
|
||||
text-shadow: none;
|
||||
border-radius: .5em 0 0 .5em;
|
||||
transition: left .3s, width .3s, padding .3s, opacity .3s;
|
||||
}
|
||||
#toast pre {
|
||||
margin: 0;
|
||||
}
|
||||
#toast.vis {
|
||||
right: 1.3em;
|
||||
transform: unset;
|
||||
}
|
||||
#toast.vis #toastc {
|
||||
left: -2em;
|
||||
width: .4em;
|
||||
padding: .3em .8em;
|
||||
opacity: 1;
|
||||
}
|
||||
#toast.inf {
|
||||
background: #07a;
|
||||
border-color: #0be;
|
||||
}
|
||||
#toast.inf #toastc {
|
||||
background: #0be;
|
||||
}
|
||||
#toast.ok {
|
||||
background: #4a0;
|
||||
border-color: #8e4;
|
||||
}
|
||||
#toast.ok #toastc {
|
||||
background: #8e4;
|
||||
}
|
||||
#toast.warn {
|
||||
background: #970;
|
||||
border-color: #fc0;
|
||||
}
|
||||
#toast.warn #toastc {
|
||||
background: #fc0;
|
||||
}
|
||||
#toast.err {
|
||||
background: #900;
|
||||
border-color: #d06;
|
||||
}
|
||||
#toast.err #toastc {
|
||||
background: #d06;
|
||||
}
|
||||
#tt.b {
|
||||
padding: 0 2em;
|
||||
border-radius: .5em;
|
||||
box-shadow: 0 .2em 1em #000;
|
||||
}
|
||||
#tt.show {
|
||||
padding: 1em 1.3em;
|
||||
border-width: .4em 0;
|
||||
height: auto;
|
||||
opacity: 1;
|
||||
}
|
||||
#tt.show.b {
|
||||
padding: 1.5em 2em;
|
||||
border-width: .5em 0;
|
||||
}
|
||||
#tt code {
|
||||
background: #3c3c3c;
|
||||
padding: .1em .3em;
|
||||
border-top: 1px solid #777;
|
||||
border-radius: .3em;
|
||||
line-height: 1.7em;
|
||||
}
|
||||
#tt em {
|
||||
color: #f6a;
|
||||
}
|
||||
#path,
|
||||
#path * {
|
||||
font-size: 1em;
|
||||
@@ -1075,6 +959,7 @@ html.light #ggrid a:hover {
|
||||
overflow: auto;
|
||||
max-height: calc(100% - 2em);
|
||||
border-bottom: .5em solid #999;
|
||||
box-shadow: 0 0 5em rgba(0,0,0,0.8);
|
||||
background: #333;
|
||||
padding: 1em;
|
||||
z-index: 765;
|
||||
@@ -1149,6 +1034,11 @@ html.light #rui {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1159,21 +1049,6 @@ html.light {
|
||||
background: #eee;
|
||||
text-shadow: none;
|
||||
}
|
||||
html.light #tt {
|
||||
background: #fff;
|
||||
border-color: #888 #000 #777 #000;
|
||||
}
|
||||
html.light #tt,
|
||||
html.light #toast {
|
||||
box-shadow: 0 .3em 1em rgba(0,0,0,0.4);
|
||||
}
|
||||
html.light #tt code {
|
||||
background: #060;
|
||||
color: #fff;
|
||||
}
|
||||
html.light #tt em {
|
||||
color: #d38;
|
||||
}
|
||||
html.light #ops,
|
||||
html.light .opbox,
|
||||
html.light #srch_form {
|
||||
@@ -1390,6 +1265,30 @@ html.light #tree::-webkit-scrollbar {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* bbox */
|
||||
|
||||
#bbox-overlay {
|
||||
display: none;
|
||||
opacity: 0;
|
||||
@@ -1568,3 +1467,331 @@ html.light #bbox-overlay figcaption a {
|
||||
0%, 100% {transform: scale(0)}
|
||||
50% {transform: scale(1)}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* upload.css */
|
||||
|
||||
#op_up2k {
|
||||
padding: 0 1em 1em 1em;
|
||||
}
|
||||
#u2form {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 2px;
|
||||
height: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
#u2form input {
|
||||
background: #444;
|
||||
border: 0px solid #444;
|
||||
outline: none;
|
||||
}
|
||||
#u2err.err {
|
||||
color: #f87;
|
||||
padding: .5em;
|
||||
}
|
||||
#u2err.msg {
|
||||
color: #999;
|
||||
padding: .5em;
|
||||
font-size: .9em;
|
||||
}
|
||||
#u2btn {
|
||||
color: #eee;
|
||||
background: #555;
|
||||
background: -moz-linear-gradient(top, #367 0%, #489 50%, #38788a 51%, #367 100%);
|
||||
background: -webkit-linear-gradient(top, #367 0%, #489 50%, #38788a 51%, #367 100%);
|
||||
background: linear-gradient(to bottom, #367 0%, #489 50%, #38788a 51%, #367 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#489', endColorstr='#38788a', GradientType=0);
|
||||
text-decoration: none;
|
||||
line-height: 1.3em;
|
||||
border: 1px solid #222;
|
||||
border-radius: .4em;
|
||||
text-align: center;
|
||||
font-size: 1.5em;
|
||||
margin: .5em auto;
|
||||
padding: .8em 0;
|
||||
width: 16em;
|
||||
cursor: pointer;
|
||||
box-shadow: .4em .4em 0 #111;
|
||||
}
|
||||
#op_up2k.srch #u2btn {
|
||||
background: linear-gradient(to bottom, #ca3 0%, #fd8 50%, #fc6 51%, #b92 100%);
|
||||
text-shadow: 1px 1px 1px #fc6;
|
||||
color: #333;
|
||||
}
|
||||
#u2conf #u2btn {
|
||||
margin: -1.5em 0;
|
||||
padding: .8em 0;
|
||||
width: 100%;
|
||||
max-width: 12em;
|
||||
display: inline-block;
|
||||
}
|
||||
#u2conf #u2btn_cw {
|
||||
text-align: right;
|
||||
}
|
||||
#u2notbtn {
|
||||
display: none;
|
||||
text-align: center;
|
||||
background: #333;
|
||||
padding-top: 1em;
|
||||
}
|
||||
#u2notbtn * {
|
||||
line-height: 1.3em;
|
||||
}
|
||||
#u2tab {
|
||||
margin: 3em auto;
|
||||
width: calc(100% - 2em);
|
||||
max-width: 100em;
|
||||
}
|
||||
#op_up2k.srch #u2tab {
|
||||
max-width: none;
|
||||
}
|
||||
#u2tab td {
|
||||
border: 1px solid #ccc;
|
||||
border-width: 0 0px 1px 0;
|
||||
padding: .1em .3em;
|
||||
}
|
||||
#u2tab td:nth-child(2) {
|
||||
width: 5em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
#u2tab td:nth-child(3) {
|
||||
width: 40%;
|
||||
}
|
||||
#op_up2k.srch td.prog {
|
||||
font-family: sans-serif;
|
||||
font-size: 1em;
|
||||
width: auto;
|
||||
}
|
||||
#u2tab tbody tr:hover td {
|
||||
background: #222;
|
||||
}
|
||||
#u2cards {
|
||||
padding: 1em 0 .3em 1em;
|
||||
margin: 1.5em auto -2.5em auto;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
#u2cards.w {
|
||||
width: 45em;
|
||||
text-align: left;
|
||||
}
|
||||
#u2cards a {
|
||||
padding: .2em 1em;
|
||||
border: 1px solid #777;
|
||||
border-width: 0 0 1px 0;
|
||||
background: linear-gradient(to bottom, #333, #222);
|
||||
}
|
||||
#u2cards a:first-child {
|
||||
border-radius: .4em 0 0 0;
|
||||
}
|
||||
#u2cards a:last-child {
|
||||
border-radius: 0 .4em 0 0;
|
||||
}
|
||||
#u2cards a.act {
|
||||
padding-bottom: .5em;
|
||||
border-width: 1px 1px .1em 1px;
|
||||
border-radius: .3em .3em 0 0;
|
||||
margin-left: -1px;
|
||||
background: linear-gradient(to bottom, #464, #333 80%);
|
||||
box-shadow: 0 -.17em .67em #280;
|
||||
border-color: #7c5 #583 #333 #583;
|
||||
position: relative;
|
||||
color: #fd7;
|
||||
}
|
||||
#u2cards span {
|
||||
color: #fff;
|
||||
}
|
||||
#u2conf {
|
||||
margin: 1em auto;
|
||||
width: 30em;
|
||||
}
|
||||
#u2conf.has_btn {
|
||||
width: 48em;
|
||||
}
|
||||
#u2conf * {
|
||||
text-align: center;
|
||||
line-height: 1em;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
#u2conf .txtbox {
|
||||
width: 3em;
|
||||
color: #fff;
|
||||
background: #444;
|
||||
border: 1px solid #777;
|
||||
font-size: 1.2em;
|
||||
padding: .15em 0;
|
||||
height: 1.05em;
|
||||
}
|
||||
#u2conf .txtbox.err {
|
||||
background: #922;
|
||||
}
|
||||
#u2conf a {
|
||||
color: #fff;
|
||||
background: #c38;
|
||||
text-decoration: none;
|
||||
border-radius: .1em;
|
||||
font-size: 1.5em;
|
||||
padding: .1em 0;
|
||||
margin: 0 -1px;
|
||||
width: 1.5em;
|
||||
height: 1em;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
bottom: -0.08em;
|
||||
}
|
||||
#u2conf input+a {
|
||||
background: #d80;
|
||||
}
|
||||
#u2conf label {
|
||||
font-size: 1.6em;
|
||||
width: 2em;
|
||||
height: 1em;
|
||||
padding: .4em 0;
|
||||
display: block;
|
||||
border-radius: .25em;
|
||||
}
|
||||
#u2conf input[type="checkbox"] {
|
||||
position: relative;
|
||||
opacity: .02;
|
||||
top: 2em;
|
||||
}
|
||||
#u2conf input[type="checkbox"]+label {
|
||||
position: relative;
|
||||
background: #603;
|
||||
border-bottom: .2em solid #a16;
|
||||
box-shadow: 0 .1em .3em #a00 inset;
|
||||
}
|
||||
#u2conf input[type="checkbox"]:checked+label {
|
||||
background: #6a1;
|
||||
border-bottom: .2em solid #efa;
|
||||
box-shadow: 0 .1em .5em #0c0;
|
||||
}
|
||||
#u2conf input[type="checkbox"]+label:hover {
|
||||
box-shadow: 0 .1em .3em #fb0;
|
||||
border-color: #fb0;
|
||||
}
|
||||
#op_up2k.srch #u2conf td:nth-child(1)>*,
|
||||
#op_up2k.srch #u2conf td:nth-child(2)>*,
|
||||
#op_up2k.srch #u2conf td:nth-child(3)>* {
|
||||
background: #777;
|
||||
border-color: #ccc;
|
||||
box-shadow: none;
|
||||
opacity: .2;
|
||||
}
|
||||
#u2foot {
|
||||
color: #fff;
|
||||
font-style: italic;
|
||||
}
|
||||
#u2foot .warn {
|
||||
font-size: 1.3em;
|
||||
padding: .5em .8em;
|
||||
margin: 1em -.6em;
|
||||
color: #f74;
|
||||
background: #322;
|
||||
border: 1px solid #633;
|
||||
border-width: .1em 0;
|
||||
text-align: center;
|
||||
}
|
||||
#u2foot .warn span {
|
||||
color: #f86;
|
||||
}
|
||||
html.light #u2foot .warn {
|
||||
color: #b00;
|
||||
background: #fca;
|
||||
border-color: #f70;
|
||||
}
|
||||
html.light #u2foot .warn span {
|
||||
color: #930;
|
||||
}
|
||||
#u2foot span {
|
||||
color: #999;
|
||||
font-size: .9em;
|
||||
font-weight: normal;
|
||||
}
|
||||
#u2footfoot {
|
||||
margin-bottom: -1em;
|
||||
}
|
||||
.prog {
|
||||
font-family: monospace, monospace;
|
||||
}
|
||||
#u2tab a>span {
|
||||
font-weight: bold;
|
||||
font-style: italic;
|
||||
color: #fff;
|
||||
padding-left: .2em;
|
||||
}
|
||||
#u2cleanup {
|
||||
float: right;
|
||||
margin-bottom: -.3em;
|
||||
}
|
||||
.fsearch_explain {
|
||||
padding-left: .7em;
|
||||
font-size: 1.1em;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
html.light #u2btn {
|
||||
box-shadow: .4em .4em 0 #ccc;
|
||||
}
|
||||
html.light #u2cards span {
|
||||
color: #000;
|
||||
}
|
||||
html.light #u2cards a {
|
||||
background: linear-gradient(to bottom, #eee, #fff);
|
||||
}
|
||||
html.light #u2cards a.act {
|
||||
color: #037;
|
||||
background: inherit;
|
||||
box-shadow: 0 -.17em .67em #0ad;
|
||||
border-color: #09c #05a #eee #05a;
|
||||
}
|
||||
html.light #u2conf .txtbox {
|
||||
background: #fff;
|
||||
color: #444;
|
||||
}
|
||||
html.light #u2conf .txtbox.err {
|
||||
background: #f96;
|
||||
color: #300;
|
||||
}
|
||||
html.light #op_up2k.srch #u2btn {
|
||||
border-color: #a80;
|
||||
}
|
||||
html.light #u2foot {
|
||||
color: #000;
|
||||
}
|
||||
html.light #u2tab tbody tr:hover td {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
<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 }}">
|
||||
<link rel="stylesheet" media="screen" href="/.cpr/ui.css?_={{ ts }}">
|
||||
<link rel="stylesheet" media="screen" href="/.cpr/browser.css?_={{ ts }}">
|
||||
{%- if css %}
|
||||
<link rel="stylesheet" type="text/css" media="screen" href="{{ css }}?_={{ ts }}">
|
||||
<link rel="stylesheet" media="screen" href="{{ css }}?_={{ ts }}">
|
||||
{%- endif %}
|
||||
</head>
|
||||
|
||||
|
||||
@@ -1283,11 +1283,12 @@ function play(tid, is_ev, seek, call_depth) {
|
||||
}
|
||||
else {
|
||||
if (call_depth !== undefined)
|
||||
return alert('failed to load ogv.js');
|
||||
return toast.err(0, 'failed to load ogv.js:\ncannot play ogg/opus in this browser\n(try a non-apple device)');
|
||||
|
||||
show_modal('<h1>loading ogv.js</h1><h2>thanks apple</h2>');
|
||||
toast.inf(0, '<h1>loading ogv.js</h1><h2>thanks apple</h2>');
|
||||
|
||||
import_js('/.cpr/deps/ogv.js', function () {
|
||||
toast.hide();
|
||||
play(tid, false, seek, 1);
|
||||
});
|
||||
|
||||
@@ -1383,7 +1384,7 @@ function evau_error(e) {
|
||||
|
||||
err += '\n\nFile: «' + uricom_dec(eplaya.src.split('/').slice(-1)[0])[0] + '»';
|
||||
|
||||
alert(err);
|
||||
toast.warn(15, err);
|
||||
}
|
||||
|
||||
|
||||
@@ -1567,7 +1568,10 @@ var fileman = (function () {
|
||||
r = {};
|
||||
|
||||
r.clip = null;
|
||||
r.bus = new BroadcastChannel("fileman_bus");
|
||||
try {
|
||||
r.bus = new BroadcastChannel("fileman_bus");
|
||||
}
|
||||
catch (ex) { }
|
||||
|
||||
r.render = function () {
|
||||
if (r.clip === null)
|
||||
@@ -1776,14 +1780,15 @@ var fileman = (function () {
|
||||
}
|
||||
}
|
||||
inew.onclick = function () {
|
||||
var name = prompt('provide a name for your new preset', ifmt.value);
|
||||
if (!name)
|
||||
return toast.warn(3, 'aborted');
|
||||
modal.prompt('provide a name for your new preset', ifmt.value, function (name) {
|
||||
if (!name)
|
||||
return toast.warn(3, 'aborted');
|
||||
|
||||
presets[name] = [ire.value, ifmt.value];
|
||||
jwrite('rn_pre', presets);
|
||||
spresets();
|
||||
ipre.value = name;
|
||||
presets[name] = [ire.value, ifmt.value];
|
||||
jwrite('rn_pre', presets);
|
||||
spresets();
|
||||
ipre.value = name;
|
||||
});
|
||||
};
|
||||
idel.onclick = function () {
|
||||
delete presets[ipre.value];
|
||||
@@ -1891,12 +1896,6 @@ var fileman = (function () {
|
||||
if (!sel.length)
|
||||
return toast.err(3, 'select at least 1 item to delete');
|
||||
|
||||
if (!confirm('===== DANGER =====\nDELETE these ' + vps.length + ' items?\n\n' + vps.join('\n')))
|
||||
return;
|
||||
|
||||
if (!confirm('Last chance! Delete?'))
|
||||
return;
|
||||
|
||||
function deleter() {
|
||||
var xhr = new XMLHttpRequest(),
|
||||
vp = vps.shift();
|
||||
@@ -1923,7 +1922,10 @@ var fileman = (function () {
|
||||
}
|
||||
deleter();
|
||||
}
|
||||
deleter();
|
||||
|
||||
modal.confirm('===== DANGER =====\nDELETE these ' + vps.length + ' items?\n\n' + uricom_adec(vps).join('\n'), function () {
|
||||
modal.confirm('Last chance! Delete?', deleter, null);
|
||||
}, null);
|
||||
};
|
||||
|
||||
r.cut = function (e) {
|
||||
@@ -1937,14 +1939,15 @@ var fileman = (function () {
|
||||
if (!sel.length)
|
||||
return toast.err(3, 'select at least 1 item to cut');
|
||||
|
||||
for (var a = 0; a < sel.length; a++) {
|
||||
vps.push(sel[a].vp);
|
||||
var cl = ebi(sel[a].id).closest('tr').classList,
|
||||
inv = cl.contains('c1');
|
||||
if (sel.length < 100)
|
||||
for (var a = 0; a < sel.length; a++) {
|
||||
vps.push(sel[a].vp);
|
||||
var cl = ebi(sel[a].id).closest('tr').classList,
|
||||
inv = cl.contains('c1');
|
||||
|
||||
cl.remove(inv ? 'c1' : 'c2');
|
||||
cl.add(inv ? 'c2' : 'c1');
|
||||
}
|
||||
cl.remove(inv ? 'c1' : 'c2');
|
||||
cl.add(inv ? 'c2' : 'c1');
|
||||
}
|
||||
|
||||
toast.inf(1, 'cut ' + sel.length + ' items');
|
||||
jwrite('fman_clip', vps);
|
||||
@@ -1981,14 +1984,11 @@ var fileman = (function () {
|
||||
}
|
||||
|
||||
if (exists.length)
|
||||
alert('these ' + exists.length + ' items cannot be pasted here (names already exist):\n\n' + uricom_adec(exists).join('\n'));
|
||||
toast.warn(30, 'these ' + exists.length + ' items cannot be pasted here (names already exist):\n\n' + uricom_adec(exists).join('\n'));
|
||||
|
||||
if (!req.length)
|
||||
return;
|
||||
|
||||
if (!confirm('paste these ' + req.length + ' items here?\n\n' + uricom_adec(req).join('\n')))
|
||||
return;
|
||||
|
||||
function paster() {
|
||||
var xhr = new XMLHttpRequest(),
|
||||
vp = req.shift();
|
||||
@@ -2018,20 +2018,26 @@ var fileman = (function () {
|
||||
}
|
||||
paster();
|
||||
}
|
||||
paster();
|
||||
|
||||
jwrite('fman_clip', []);
|
||||
modal.confirm('paste these ' + req.length + ' items here?\n\n' + uricom_adec(req).join('\n'), function () {
|
||||
paster();
|
||||
jwrite('fman_clip', []);
|
||||
}, null);
|
||||
};
|
||||
|
||||
r.bus.onmessage = function (e) {
|
||||
r.clip = null;
|
||||
r.render();
|
||||
var me = get_evpath();
|
||||
if (e && e.data == me)
|
||||
treectl.goto(e.data);
|
||||
};
|
||||
if (r.bus)
|
||||
r.bus.onmessage = function (e) {
|
||||
r.clip = null;
|
||||
r.render();
|
||||
var me = get_evpath();
|
||||
if (e && e.data == me)
|
||||
treectl.goto(e.data);
|
||||
};
|
||||
|
||||
r.tx = function (msg) {
|
||||
if (!r.bus)
|
||||
return;
|
||||
|
||||
r.bus.postMessage(msg);
|
||||
r.bus.onmessage();
|
||||
};
|
||||
@@ -3689,6 +3695,16 @@ var msel = (function () {
|
||||
return;
|
||||
|
||||
r.sel = [];
|
||||
if (r.all && r.all.length) {
|
||||
for (var a = 0; a < r.all.length; a++) {
|
||||
var ao = r.all[a];
|
||||
ao.sel = ebi(ao.id).closest('tr').classList.contains('sel');
|
||||
if (ao.sel)
|
||||
r.sel.push(ao);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
r.all = [];
|
||||
var links = QSA('#files tbody td:nth-child(2) a:last-child'),
|
||||
vbase = get_evpath();
|
||||
@@ -3717,8 +3733,11 @@ var msel = (function () {
|
||||
r.load();
|
||||
return r.all;
|
||||
};
|
||||
r.selui = function () {
|
||||
r.sel = r.all = null;
|
||||
r.selui = function (reset) {
|
||||
r.sel = null;
|
||||
if (reset)
|
||||
r.all = null;
|
||||
|
||||
clmod(ebi('wtoggle'), 'sel', r.getsel().length);
|
||||
thegrid.loadsel();
|
||||
fileman.render();
|
||||
@@ -3777,7 +3796,7 @@ var msel = (function () {
|
||||
for (var a = 0, aa = tds.length; a < aa; a++) {
|
||||
tds[a].onclick = r.seltgl;
|
||||
}
|
||||
r.selui();
|
||||
r.selui(true);
|
||||
arcfmt.render();
|
||||
fileman.render();
|
||||
ebi('selzip').style.display = ebi('unsearch') ? 'none' : '';
|
||||
|
||||
@@ -8,140 +8,6 @@ html, body {
|
||||
font-family: sans-serif;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
|
||||
|
||||
#tt, #toast {
|
||||
position: fixed;
|
||||
max-width: 34em;
|
||||
background: #222;
|
||||
border: 0 solid #777;
|
||||
box-shadow: 0 .2em .5em #222;
|
||||
border-radius: .4em;
|
||||
z-index: 9001;
|
||||
}
|
||||
#tt {
|
||||
overflow: hidden;
|
||||
margin-top: 1em;
|
||||
padding: 0 1.3em;
|
||||
height: 0;
|
||||
opacity: .1;
|
||||
transition: opacity 0.14s, height 0.14s, padding 0.14s;
|
||||
}
|
||||
#toast {
|
||||
top: 1.4em;
|
||||
right: -1em;
|
||||
line-height: 1.5em;
|
||||
padding: 1em 1.3em;
|
||||
border-width: .4em 0;
|
||||
transform: translateX(100%);
|
||||
transition:
|
||||
transform .4s cubic-bezier(.2, 1.2, .5, 1),
|
||||
right .4s cubic-bezier(.2, 1.2, .5, 1);
|
||||
text-shadow: 1px 1px 0 #000;
|
||||
color: #fff;
|
||||
}
|
||||
#toast pre {
|
||||
margin: 0;
|
||||
}
|
||||
#toastc {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
left: 0;
|
||||
width: 0;
|
||||
opacity: 0;
|
||||
padding: .3em 0;
|
||||
margin: -.3em 0 0 0;
|
||||
line-height: 1.5em;
|
||||
color: #000;
|
||||
border: none;
|
||||
outline: none;
|
||||
text-shadow: none;
|
||||
border-radius: .5em 0 0 .5em;
|
||||
transition: left .3s, width .3s, padding .3s, opacity .3s;
|
||||
}
|
||||
#toast.vis {
|
||||
right: 1.3em;
|
||||
transform: unset;
|
||||
}
|
||||
#toast.vis #toastc {
|
||||
left: -2em;
|
||||
width: .4em;
|
||||
padding: .3em .8em;
|
||||
opacity: 1;
|
||||
}
|
||||
#toast.inf {
|
||||
background: #07a;
|
||||
border-color: #0be;
|
||||
}
|
||||
#toast.inf #toastc {
|
||||
background: #0be;
|
||||
}
|
||||
#toast.ok {
|
||||
background: #4a0;
|
||||
border-color: #8e4;
|
||||
}
|
||||
#toast.ok #toastc {
|
||||
background: #8e4;
|
||||
}
|
||||
#toast.warn {
|
||||
background: #970;
|
||||
border-color: #fc0;
|
||||
}
|
||||
#toast.warn #toastc {
|
||||
background: #fc0;
|
||||
}
|
||||
#toast.err {
|
||||
background: #900;
|
||||
border-color: #d06;
|
||||
}
|
||||
#toast.err #toastc {
|
||||
background: #d06;
|
||||
}
|
||||
#tt.b {
|
||||
padding: 0 2em;
|
||||
border-radius: .5em;
|
||||
box-shadow: 0 .2em 1em #000;
|
||||
}
|
||||
#tt.show {
|
||||
padding: 1em 1.3em;
|
||||
border-width: .4em 0;
|
||||
height: auto;
|
||||
opacity: 1;
|
||||
}
|
||||
#tt.show.b {
|
||||
padding: 1.5em 2em;
|
||||
border-width: .5em 0;
|
||||
}
|
||||
#tt code {
|
||||
background: #3c3c3c;
|
||||
padding: .1em .3em;
|
||||
border-top: 1px solid #777;
|
||||
border-radius: .3em;
|
||||
line-height: 1.7em;
|
||||
}
|
||||
#tt em {
|
||||
color: #f6a;
|
||||
}
|
||||
html.light #tt {
|
||||
background: #fff;
|
||||
border-color: #888 #000 #777 #000;
|
||||
}
|
||||
html.light #tt,
|
||||
html.light #toast {
|
||||
box-shadow: 0 .3em 1em rgba(0,0,0,0.4);
|
||||
}
|
||||
html.light #tt code {
|
||||
background: #060;
|
||||
color: #fff;
|
||||
}
|
||||
html.light #tt em {
|
||||
color: #d38;
|
||||
}
|
||||
|
||||
|
||||
|
||||
#mtw {
|
||||
display: none;
|
||||
}
|
||||
@@ -149,6 +15,10 @@ html.light #tt em {
|
||||
margin: 0 auto;
|
||||
padding: 0 1.5em;
|
||||
}
|
||||
#toast {
|
||||
bottom: auto;
|
||||
top: 1.4em;
|
||||
}
|
||||
pre, code, a {
|
||||
color: #480;
|
||||
background: #f7f7f7;
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<!DOCTYPE html><html><head>
|
||||
<meta charset="utf-8">
|
||||
<title>📝🎉 {{ title }}</title> <!-- 📜 -->
|
||||
<title>📝🎉 {{ title }}</title>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=0.7">
|
||||
<link href="/.cpr/md.css?_={{ ts }}" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/.cpr/ui.css?_={{ ts }}">
|
||||
<link rel="stylesheet" href="/.cpr/md.css?_={{ ts }}">
|
||||
{%- if edit %}
|
||||
<link href="/.cpr/md2.css?_={{ ts }}" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/.cpr/md2.css?_={{ ts }}">
|
||||
{%- endif %}
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -185,7 +185,7 @@ function md_plug_err(ex, js) {
|
||||
errbox.style.cssText = 'position:absolute;top:0;left:0;padding:1em .5em;background:#2b2b2b;color:#fc5'
|
||||
errbox.textContent = msg;
|
||||
errbox.onclick = function () {
|
||||
alert('' + ex.stack);
|
||||
modal.alert('<pre>' + ex.stack + '</pre>');
|
||||
};
|
||||
if (o) {
|
||||
errbox.appendChild(o);
|
||||
|
||||
@@ -326,26 +326,32 @@ function save(e) {
|
||||
return toast.inf(2, "no changes");
|
||||
|
||||
var force = (save_cls.indexOf('force-save') >= 0);
|
||||
if (force && !confirm('confirm that you wish to lose the changes made on the server since you opened this document'))
|
||||
return toast.inf(3, 'aborted');
|
||||
function save2() {
|
||||
var txt = dom_src.value,
|
||||
fd = new FormData();
|
||||
|
||||
var txt = dom_src.value;
|
||||
fd.append("act", "tput");
|
||||
fd.append("lastmod", (force ? -1 : last_modified));
|
||||
fd.append("body", txt);
|
||||
|
||||
var fd = new FormData();
|
||||
fd.append("act", "tput");
|
||||
fd.append("lastmod", (force ? -1 : last_modified));
|
||||
fd.append("body", txt);
|
||||
var url = (document.location + '').split('?')[0];
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', url, true);
|
||||
xhr.responseType = 'text';
|
||||
xhr.onreadystatechange = save_cb;
|
||||
xhr.btn = save_btn;
|
||||
xhr.txt = txt;
|
||||
|
||||
var url = (document.location + '').split('?')[0];
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', url, true);
|
||||
xhr.responseType = 'text';
|
||||
xhr.onreadystatechange = save_cb;
|
||||
xhr.btn = save_btn;
|
||||
xhr.txt = txt;
|
||||
modpoll.skip_one = true; // skip one iteration while we save
|
||||
xhr.send(fd);
|
||||
}
|
||||
|
||||
modpoll.skip_one = true; // skip one iteration while we save
|
||||
xhr.send(fd);
|
||||
if (!force)
|
||||
save2();
|
||||
else
|
||||
modal.confirm('confirm that you wish to lose the changes made on the server since you opened this document', save2, function () {
|
||||
toast.inf(3, 'aborted');
|
||||
});
|
||||
}
|
||||
|
||||
function save_cb() {
|
||||
@@ -353,14 +359,14 @@ function save_cb() {
|
||||
return;
|
||||
|
||||
if (this.status !== 200)
|
||||
return alert('Error! The file was NOT saved.\n\n' + this.status + ": " + (this.responseText + '').replace(/^<pre>/, ""));
|
||||
return toast.err(0, 'Error! The file was NOT saved.\n\n' + this.status + ": " + (this.responseText + '').replace(/^<pre>/, ""));
|
||||
|
||||
var r;
|
||||
try {
|
||||
r = JSON.parse(this.responseText);
|
||||
}
|
||||
catch (ex) {
|
||||
return alert('Failed to parse reply from server:\n\n' + this.responseText);
|
||||
return toast.err(0, 'Failed to parse reply from server:\n\n' + this.responseText);
|
||||
}
|
||||
|
||||
if (!r.ok) {
|
||||
@@ -375,12 +381,10 @@ function save_cb() {
|
||||
r.lastmod + ' lastmod on the server now,',
|
||||
r.now + ' server time now,\n',
|
||||
];
|
||||
alert(msg.join('\n'));
|
||||
return toast.err(0, msg.join('\n'));
|
||||
}
|
||||
else {
|
||||
alert('Error! Save failed. Maybe this JSON explains why:\n\n' + this.responseText);
|
||||
}
|
||||
return;
|
||||
else
|
||||
return toast.err(0, 'Error! Save failed. Maybe this JSON explains why:\n\n' + this.responseText);
|
||||
}
|
||||
|
||||
this.btn.classList.remove('force-save');
|
||||
@@ -407,10 +411,8 @@ function savechk_cb() {
|
||||
if (this.readyState != XMLHttpRequest.DONE)
|
||||
return;
|
||||
|
||||
if (this.status !== 200) {
|
||||
alert('Error! The file was NOT saved.\n\n' + this.status + ": " + (this.responseText + '').replace(/^<pre>/, ""));
|
||||
return;
|
||||
}
|
||||
if (this.status !== 200)
|
||||
return toast.err(0, 'Error! The file was NOT saved.\n\n' + this.status + ": " + (this.responseText + '').replace(/^<pre>/, ""));
|
||||
|
||||
var doc1 = this.txt.replace(/\r\n/g, "\n");
|
||||
var doc2 = this.responseText.replace(/\r\n/g, "\n");
|
||||
@@ -423,12 +425,12 @@ function savechk_cb() {
|
||||
}, 100);
|
||||
return;
|
||||
}
|
||||
alert(
|
||||
modal.alert(
|
||||
'Error! The document on the server does not appear to have saved correctly (your editor contents and the server copy is not identical). Place the document on your clipboard for now and check the server logs for hints\n\n' +
|
||||
'Length: yours=' + doc1.length + ', server=' + doc2.length
|
||||
);
|
||||
alert('yours, ' + doc1.length + ' byte:\n[' + doc1 + ']');
|
||||
alert('server, ' + doc2.length + ' byte:\n[' + doc2 + ']');
|
||||
modal.alert('yours, ' + doc1.length + ' byte:\n[' + doc1 + ']');
|
||||
modal.alert('server, ' + doc2.length + ' byte:\n[' + doc2 + ']');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -865,12 +867,10 @@ function iter_uni(e) {
|
||||
function cfg_uni(e) {
|
||||
if (e) e.preventDefault();
|
||||
|
||||
var reply = prompt("unicode whitelist", esc_uni_whitelist);
|
||||
if (reply === null)
|
||||
return;
|
||||
|
||||
esc_uni_whitelist = reply;
|
||||
js_uni_whitelist = eval('\'' + esc_uni_whitelist + '\'');
|
||||
modal.prompt("unicode whitelist", esc_uni_whitelist, function (reply) {
|
||||
esc_uni_whitelist = reply;
|
||||
js_uni_whitelist = eval('\'' + esc_uni_whitelist + '\'');
|
||||
}, null);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -18,6 +18,10 @@ html, body {
|
||||
background: #f7f7f7;
|
||||
color: #333;
|
||||
}
|
||||
#toast {
|
||||
bottom: auto;
|
||||
top: 1.4em;
|
||||
}
|
||||
#mn {
|
||||
font-weight: normal;
|
||||
margin: 1.3em 0 .7em 1em;
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
<title>📝🎉 {{ title }}</title>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=0.7">
|
||||
<link href="/.cpr/mde.css?_={{ ts }}" rel="stylesheet">
|
||||
<link href="/.cpr/deps/mini-fa.css?_={{ ts }}" rel="stylesheet">
|
||||
<link href="/.cpr/deps/easymde.css?_={{ ts }}" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/.cpr/ui.css?_={{ ts }}">
|
||||
<link rel="stylesheet" href="/.cpr/mde.css?_={{ ts }}">
|
||||
<link rel="stylesheet" href="/.cpr/deps/mini-fa.css?_={{ ts }}">
|
||||
<link rel="stylesheet" href="/.cpr/deps/easymde.css?_={{ ts }}">
|
||||
</head>
|
||||
<body>
|
||||
<div id="mw">
|
||||
|
||||
@@ -110,25 +110,31 @@ function save(mde) {
|
||||
return toast.inf(2, 'no changes');
|
||||
|
||||
var force = save_btn.classList.contains('force-save');
|
||||
if (force && !confirm('confirm that you wish to lose the changes made on the server since you opened this document'))
|
||||
return toast.inf(3, 'aborted');
|
||||
function save2() {
|
||||
var txt = mde.value();
|
||||
|
||||
var txt = mde.value();
|
||||
var fd = new FormData();
|
||||
fd.append("act", "tput");
|
||||
fd.append("lastmod", (force ? -1 : last_modified));
|
||||
fd.append("body", txt);
|
||||
|
||||
var fd = new FormData();
|
||||
fd.append("act", "tput");
|
||||
fd.append("lastmod", (force ? -1 : last_modified));
|
||||
fd.append("body", txt);
|
||||
var url = (document.location + '').split('?')[0];
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', url, true);
|
||||
xhr.responseType = 'text';
|
||||
xhr.onreadystatechange = save_cb;
|
||||
xhr.btn = save_btn;
|
||||
xhr.mde = mde;
|
||||
xhr.txt = txt;
|
||||
xhr.send(fd);
|
||||
}
|
||||
|
||||
var url = (document.location + '').split('?')[0];
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', url, true);
|
||||
xhr.responseType = 'text';
|
||||
xhr.onreadystatechange = save_cb;
|
||||
xhr.btn = save_btn;
|
||||
xhr.mde = mde;
|
||||
xhr.txt = txt;
|
||||
xhr.send(fd);
|
||||
if (!force)
|
||||
save2();
|
||||
else
|
||||
modal.confirm('confirm that you wish to lose the changes made on the server since you opened this document', save2, function () {
|
||||
toast.inf(3, 'aborted');
|
||||
});
|
||||
}
|
||||
|
||||
function save_cb() {
|
||||
@@ -136,14 +142,14 @@ function save_cb() {
|
||||
return;
|
||||
|
||||
if (this.status !== 200)
|
||||
return alert('Error! The file was NOT saved.\n\n' + this.status + ": " + (this.responseText + '').replace(/^<pre>/, ""));
|
||||
return toast.err(0, 'Error! The file was NOT saved.\n\n' + this.status + ": " + (this.responseText + '').replace(/^<pre>/, ""));
|
||||
|
||||
var r;
|
||||
try {
|
||||
r = JSON.parse(this.responseText);
|
||||
}
|
||||
catch (ex) {
|
||||
return alert('Failed to parse reply from server:\n\n' + this.responseText);
|
||||
return toast.err(0, 'Failed to parse reply from server:\n\n' + this.responseText);
|
||||
}
|
||||
|
||||
if (!r.ok) {
|
||||
@@ -158,12 +164,10 @@ function save_cb() {
|
||||
r.lastmod + ' lastmod on the server now,',
|
||||
r.now + ' server time now,\n',
|
||||
];
|
||||
alert(msg.join('\n'));
|
||||
return toast.err(0, msg.join('\n'));
|
||||
}
|
||||
else {
|
||||
alert('Error! Save failed. Maybe this JSON explains why:\n\n' + this.responseText);
|
||||
}
|
||||
return;
|
||||
else
|
||||
return toast.err(0, 'Error! Save failed. Maybe this JSON explains why:\n\n' + this.responseText);
|
||||
}
|
||||
|
||||
this.btn.classList.remove('force-save');
|
||||
@@ -186,35 +190,23 @@ function save_chk() {
|
||||
if (this.readyState != XMLHttpRequest.DONE)
|
||||
return;
|
||||
|
||||
if (this.status !== 200) {
|
||||
alert('Error! The file was NOT saved.\n\n' + this.status + ": " + (this.responseText + '').replace(/^<pre>/, ""));
|
||||
return;
|
||||
}
|
||||
if (this.status !== 200)
|
||||
return toast.err(0, 'Error! The file was NOT saved.\n\n' + this.status + ": " + (this.responseText + '').replace(/^<pre>/, ""));
|
||||
|
||||
var doc1 = this.txt.replace(/\r\n/g, "\n");
|
||||
var doc2 = this.responseText.replace(/\r\n/g, "\n");
|
||||
if (doc1 != doc2) {
|
||||
alert(
|
||||
modal.alert(
|
||||
'Error! The document on the server does not appear to have saved correctly (your editor contents and the server copy is not identical). Place the document on your clipboard for now and check the server logs for hints\n\n' +
|
||||
'Length: yours=' + doc1.length + ', server=' + doc2.length
|
||||
);
|
||||
alert('yours, ' + doc1.length + ' byte:\n[' + doc1 + ']');
|
||||
alert('server, ' + doc2.length + ' byte:\n[' + doc2 + ']');
|
||||
modal.alert('yours, ' + doc1.length + ' byte:\n[' + doc1 + ']');
|
||||
modal.alert('server, ' + doc2.length + ' byte:\n[' + doc2 + ']');
|
||||
return;
|
||||
}
|
||||
|
||||
last_modified = this.lastmod;
|
||||
md_changed(this.mde, true);
|
||||
|
||||
var ok = mknod('div');
|
||||
ok.setAttribute('style', 'font-size:6em;font-family:serif;font-weight:bold;color:#cf6;background:#444;border-radius:.3em;padding:.6em 0;position:fixed;top:30%;left:calc(50% - 2em);width:4em;text-align:center;z-index:9001;transition:opacity 0.2s ease-in-out;opacity:1');
|
||||
ok.innerHTML = 'OK✔️';
|
||||
var parent = ebi('m');
|
||||
document.documentElement.appendChild(ok);
|
||||
setTimeout(function () {
|
||||
ok.style.opacity = 0;
|
||||
}, 500);
|
||||
setTimeout(function () {
|
||||
ok.parentNode.removeChild(ok);
|
||||
}, 750);
|
||||
toast.ok(2, 'save OK' + (this.ntry ? '\nattempt ' + this.ntry : ''));
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<title>copyparty</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/msg.css?_={{ ts }}">
|
||||
<link rel="stylesheet" media="screen" href="/.cpr/msg.css?_={{ ts }}">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<title>copyparty</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/splash.css?_={{ ts }}">
|
||||
<link rel="stylesheet" media="screen" href="/.cpr/splash.css?_={{ ts }}">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
199
copyparty/web/ui.css
Normal file
199
copyparty/web/ui.css
Normal file
@@ -0,0 +1,199 @@
|
||||
#tt, #toast {
|
||||
position: fixed;
|
||||
max-width: 34em;
|
||||
background: #222;
|
||||
border: 0 solid #777;
|
||||
box-shadow: 0 .2em .5em #222;
|
||||
border-radius: .4em;
|
||||
z-index: 9001;
|
||||
}
|
||||
#tt {
|
||||
overflow: hidden;
|
||||
margin-top: 1em;
|
||||
padding: 0 1.3em;
|
||||
height: 0;
|
||||
opacity: .1;
|
||||
transition: opacity 0.14s, height 0.14s, padding 0.14s;
|
||||
}
|
||||
#toast {
|
||||
bottom: 5em;
|
||||
right: -1em;
|
||||
line-height: 1.5em;
|
||||
padding: 1em 1.3em;
|
||||
border-width: .4em 0;
|
||||
transform: translateX(100%);
|
||||
transition:
|
||||
transform .4s cubic-bezier(.2, 1.2, .5, 1),
|
||||
right .4s cubic-bezier(.2, 1.2, .5, 1);
|
||||
text-shadow: 1px 1px 0 #000;
|
||||
color: #fff;
|
||||
}
|
||||
#toastc {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
left: 0;
|
||||
width: 0;
|
||||
opacity: 0;
|
||||
padding: .3em 0;
|
||||
margin: -.3em 0 0 0;
|
||||
line-height: 1.5em;
|
||||
color: #000;
|
||||
border: none;
|
||||
outline: none;
|
||||
text-shadow: none;
|
||||
border-radius: .5em 0 0 .5em;
|
||||
transition: left .3s, width .3s, padding .3s, opacity .3s;
|
||||
}
|
||||
#toast pre {
|
||||
margin: 0;
|
||||
}
|
||||
#toast.vis {
|
||||
right: 1.3em;
|
||||
transform: unset;
|
||||
}
|
||||
#toast.vis #toastc {
|
||||
left: -2em;
|
||||
width: .4em;
|
||||
padding: .3em .8em;
|
||||
opacity: 1;
|
||||
}
|
||||
#toast.inf {
|
||||
background: #07a;
|
||||
border-color: #0be;
|
||||
}
|
||||
#toast.inf #toastc {
|
||||
background: #0be;
|
||||
}
|
||||
#toast.ok {
|
||||
background: #380;
|
||||
border-color: #8e4;
|
||||
}
|
||||
#toast.ok #toastc {
|
||||
background: #8e4;
|
||||
}
|
||||
#toast.warn {
|
||||
background: #970;
|
||||
border-color: #fc0;
|
||||
}
|
||||
#toast.warn #toastc {
|
||||
background: #fc0;
|
||||
}
|
||||
#toast.err {
|
||||
background: #900;
|
||||
border-color: #d06;
|
||||
}
|
||||
#toast.err #toastc {
|
||||
background: #d06;
|
||||
}
|
||||
#tt.b {
|
||||
padding: 0 2em;
|
||||
border-radius: .5em;
|
||||
box-shadow: 0 .2em 1em #000;
|
||||
}
|
||||
#tt.show {
|
||||
padding: 1em 1.3em;
|
||||
border-width: .4em 0;
|
||||
height: auto;
|
||||
opacity: 1;
|
||||
}
|
||||
#tt.show.b {
|
||||
padding: 1.5em 2em;
|
||||
border-width: .5em 0;
|
||||
}
|
||||
#tt code {
|
||||
background: #3c3c3c;
|
||||
padding: .1em .3em;
|
||||
border-top: 1px solid #777;
|
||||
border-radius: .3em;
|
||||
line-height: 1.7em;
|
||||
}
|
||||
#tt em {
|
||||
color: #f6a;
|
||||
}
|
||||
html.light #tt {
|
||||
background: #fff;
|
||||
border-color: #888 #000 #777 #000;
|
||||
}
|
||||
html.light #tt,
|
||||
html.light #toast {
|
||||
box-shadow: 0 .3em 1em rgba(0,0,0,0.4);
|
||||
}
|
||||
html.light #tt code {
|
||||
background: #060;
|
||||
color: #fff;
|
||||
}
|
||||
html.light #tt em {
|
||||
color: #d38;
|
||||
}
|
||||
#modal {
|
||||
position: fixed;
|
||||
overflow: auto;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 9001;
|
||||
background: rgba(64,64,64,0.6);
|
||||
}
|
||||
#modal>table {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
#modal td {
|
||||
text-align: center;
|
||||
}
|
||||
#modalc {
|
||||
display: inline-block;
|
||||
background: #f7f7f7;
|
||||
color: #333;
|
||||
text-shadow: none;
|
||||
text-align: left;
|
||||
margin: 3em;
|
||||
padding: 1em 1.1em;
|
||||
border-radius: .6em;
|
||||
box-shadow: 0 .3em 3em rgba(0,0,0,0.5);
|
||||
max-width: 50em;
|
||||
max-height: 30em;
|
||||
overflow: auto;
|
||||
}
|
||||
@media (min-width: 40em) {
|
||||
#modalc {
|
||||
min-width: 30em;
|
||||
}
|
||||
}
|
||||
#modalb {
|
||||
text-align: right;
|
||||
padding-top: 1em;
|
||||
}
|
||||
#modalb a {
|
||||
color: #000;
|
||||
background: #ccc;
|
||||
display: inline-block;
|
||||
border-radius: .3em;
|
||||
padding: .5em 1em;
|
||||
outline: none;
|
||||
border: none;
|
||||
}
|
||||
#modalb a:focus,
|
||||
#modalb a:hover {
|
||||
background: #06d;
|
||||
color: #fff;
|
||||
}
|
||||
#modalb a+a {
|
||||
margin-left: .5em;
|
||||
}
|
||||
#modali {
|
||||
display: block;
|
||||
width: calc(100% - 1.25em);
|
||||
margin: 1em -.1em 0 -.1em;
|
||||
padding: .5em;
|
||||
outline: none;
|
||||
border: .25em solid #ccc;
|
||||
border-radius: .4em;
|
||||
}
|
||||
#modali:focus {
|
||||
border-color: #06d;
|
||||
}
|
||||
@@ -97,13 +97,11 @@ function up2k_flagbus() {
|
||||
}
|
||||
};
|
||||
var do_take = function (now) {
|
||||
//dbg('*', 'do_take');
|
||||
tx(now, "have");
|
||||
flag.owner = [flag.id, now];
|
||||
flag.ours = true;
|
||||
};
|
||||
var do_want = function (now) {
|
||||
//dbg('*', 'do_want');
|
||||
tx(now, "want");
|
||||
};
|
||||
flag.take = function (now) {
|
||||
@@ -135,15 +133,16 @@ function up2k_flagbus() {
|
||||
|
||||
|
||||
function U2pvis(act, btns) {
|
||||
this.act = act;
|
||||
this.ctr = { "ok": 0, "ng": 0, "bz": 0, "q": 0 };
|
||||
this.tab = [];
|
||||
this.head = 0;
|
||||
this.tail = -1;
|
||||
this.wsz = 3;
|
||||
var r = this;
|
||||
r.act = act;
|
||||
r.ctr = { "ok": 0, "ng": 0, "bz": 0, "q": 0 };
|
||||
r.tab = [];
|
||||
r.head = 0;
|
||||
r.tail = -1;
|
||||
r.wsz = 3;
|
||||
|
||||
this.addfile = function (entry, sz, draw) {
|
||||
this.tab.push({
|
||||
r.addfile = function (entry, sz, draw) {
|
||||
r.tab.push({
|
||||
"hn": entry[0],
|
||||
"ht": entry[1],
|
||||
"hp": entry[2],
|
||||
@@ -155,34 +154,34 @@ function U2pvis(act, btns) {
|
||||
"bd": 0, // bytes done
|
||||
"bd0": 0 // upload start
|
||||
});
|
||||
this.ctr["q"]++;
|
||||
r.ctr["q"]++;
|
||||
if (!draw)
|
||||
return;
|
||||
|
||||
this.drawcard("q");
|
||||
if (this.act == "q") {
|
||||
this.addrow(this.tab.length - 1);
|
||||
r.drawcard("q");
|
||||
if (r.act == "q") {
|
||||
r.addrow(r.tab.length - 1);
|
||||
}
|
||||
if (this.act == "bz") {
|
||||
this.bzw();
|
||||
if (r.act == "bz") {
|
||||
r.bzw();
|
||||
}
|
||||
};
|
||||
|
||||
this.is_act = function (card) {
|
||||
if (this.act == "done")
|
||||
r.is_act = function (card) {
|
||||
if (r.act == "done")
|
||||
return card == "ok" || card == "ng";
|
||||
|
||||
return this.act == card;
|
||||
return r.act == card;
|
||||
}
|
||||
|
||||
this.seth = function (nfile, field, html) {
|
||||
var fo = this.tab[nfile];
|
||||
r.seth = function (nfile, field, html) {
|
||||
var fo = r.tab[nfile];
|
||||
field = ['hn', 'ht', 'hp'][field];
|
||||
if (fo[field] === html)
|
||||
return;
|
||||
|
||||
fo[field] = html;
|
||||
if (!this.is_act(fo.in))
|
||||
if (!r.is_act(fo.in))
|
||||
return;
|
||||
|
||||
var obj = ebi('f{0}{1}'.format(nfile, field.slice(1)));
|
||||
@@ -193,26 +192,26 @@ function U2pvis(act, btns) {
|
||||
}
|
||||
};
|
||||
|
||||
this.setab = function (nfile, nblocks) {
|
||||
r.setab = function (nfile, nblocks) {
|
||||
var t = [];
|
||||
for (var a = 0; a < nblocks; a++)
|
||||
t.push(0);
|
||||
|
||||
this.tab[nfile].cb = t;
|
||||
r.tab[nfile].cb = t;
|
||||
};
|
||||
|
||||
this.setat = function (nfile, blocktab) {
|
||||
this.tab[nfile].cb = blocktab;
|
||||
r.setat = function (nfile, blocktab) {
|
||||
r.tab[nfile].cb = blocktab;
|
||||
|
||||
var bd = 0;
|
||||
for (var a = 0; a < blocktab.length; a++)
|
||||
bd += blocktab[a];
|
||||
|
||||
this.tab[nfile].bd = bd;
|
||||
this.tab[nfile].bd0 = bd;
|
||||
r.tab[nfile].bd = bd;
|
||||
r.tab[nfile].bd0 = bd;
|
||||
};
|
||||
|
||||
this.perc = function (bd, bd0, sz, t0) {
|
||||
r.perc = function (bd, bd0, sz, t0) {
|
||||
var td = Date.now() - t0,
|
||||
p = bd * 100.0 / sz,
|
||||
nb = bd - bd0,
|
||||
@@ -222,15 +221,15 @@ function U2pvis(act, btns) {
|
||||
return [p, s2ms(eta), spd / (1024 * 1024)];
|
||||
};
|
||||
|
||||
this.hashed = function (fobj) {
|
||||
var fo = this.tab[fobj.n],
|
||||
r.hashed = function (fobj) {
|
||||
var fo = r.tab[fobj.n],
|
||||
nb = fo.bt * (++fo.nh / fo.cb.length),
|
||||
p = this.perc(nb, 0, fobj.size, fobj.t_hashing);
|
||||
p = r.perc(nb, 0, fobj.size, fobj.t_hashing);
|
||||
|
||||
fo.hp = '{0}%, {1}, {2} MB/s'.format(
|
||||
p[0].toFixed(2), p[1], p[2].toFixed(2)
|
||||
);
|
||||
if (!this.is_act(fo.in))
|
||||
if (!r.is_act(fo.in))
|
||||
return;
|
||||
|
||||
var obj = ebi('f{0}p'.format(fobj.n)),
|
||||
@@ -241,31 +240,31 @@ function U2pvis(act, btns) {
|
||||
obj.style.background = 'linear-gradient(90deg, #025, #06a ' + o1 + '%, #09d ' + o2 + '%, #333 ' + o3 + '%, #333 99%, #777)';
|
||||
};
|
||||
|
||||
this.prog = function (fobj, nchunk, cbd) {
|
||||
var fo = this.tab[fobj.n],
|
||||
r.prog = function (fobj, nchunk, cbd) {
|
||||
var fo = r.tab[fobj.n],
|
||||
delta = cbd - fo.cb[nchunk];
|
||||
|
||||
fo.cb[nchunk] = cbd;
|
||||
fo.bd += delta;
|
||||
|
||||
var p = this.perc(fo.bd, fo.bd0, fo.bt, fobj.t_uploading);
|
||||
var p = r.perc(fo.bd, fo.bd0, fo.bt, fobj.t_uploading);
|
||||
fo.hp = '{0}%, {1}, {2} MB/s'.format(
|
||||
p[0].toFixed(2), p[1], p[2].toFixed(2)
|
||||
);
|
||||
|
||||
if (!this.is_act(fo.in))
|
||||
if (!r.is_act(fo.in))
|
||||
return;
|
||||
|
||||
var obj = ebi('f{0}p'.format(fobj.n)),
|
||||
o1 = p[0] - 2, o2 = p[0] - 0.1, o3 = p[0];
|
||||
|
||||
if (!obj) { //} || true) {
|
||||
if (!obj) {
|
||||
var msg = [
|
||||
"act", this.act,
|
||||
"act", r.act,
|
||||
"in", fo.in,
|
||||
"is_act", this.is_act(fo.in),
|
||||
"head", this.head,
|
||||
"tail", this.tail,
|
||||
"is_act", r.is_act(fo.in),
|
||||
"head", r.head,
|
||||
"tail", r.tail,
|
||||
"nfile", fobj.n,
|
||||
"name", fobj.name,
|
||||
"sz", fobj.size,
|
||||
@@ -283,12 +282,12 @@ function U2pvis(act, btns) {
|
||||
for (var a = 0, aa = ds.length; a < aa; a++) {
|
||||
var id = ds[a].parentNode.getAttribute('id').slice(1, -1);
|
||||
console.log("dom %d/%d = [%s] in(%s) is_act(%s) %s",
|
||||
a, aa, id, this.tab[id].in, this.is_act(fo.in), ds[a].textContent);
|
||||
a, aa, id, r.tab[id].in, r.is_act(fo.in), ds[a].textContent);
|
||||
}
|
||||
|
||||
for (var a = 0, aa = this.tab.length; a < aa; a++)
|
||||
if (this.is_act(this.tab[a].in))
|
||||
console.log("tab %d/%d = sz %s", a, aa, this.tab[a].bt);
|
||||
for (var a = 0, aa = r.tab.length; a < aa; a++)
|
||||
if (r.is_act(r.tab[a].in))
|
||||
console.log("tab %d/%d = sz %s", a, aa, r.tab[a].bt);
|
||||
|
||||
throw new Error('see console');
|
||||
}
|
||||
@@ -298,27 +297,27 @@ function U2pvis(act, btns) {
|
||||
obj.style.background = 'linear-gradient(90deg, #050, #270 ' + o1 + '%, #4b0 ' + o2 + '%, #333 ' + o3 + '%, #333 99%, #777)';
|
||||
};
|
||||
|
||||
this.move = function (nfile, newcat) {
|
||||
var fo = this.tab[nfile],
|
||||
r.move = function (nfile, newcat) {
|
||||
var fo = r.tab[nfile],
|
||||
oldcat = fo.in,
|
||||
bz_act = this.act == "bz";
|
||||
bz_act = r.act == "bz";
|
||||
|
||||
if (oldcat == newcat)
|
||||
return;
|
||||
|
||||
fo.in = newcat;
|
||||
this.ctr[oldcat]--;
|
||||
this.ctr[newcat]++;
|
||||
this.drawcard(oldcat);
|
||||
this.drawcard(newcat);
|
||||
if (this.is_act(newcat)) {
|
||||
this.tail = Math.max(this.tail, nfile + 1);
|
||||
r.ctr[oldcat]--;
|
||||
r.ctr[newcat]++;
|
||||
r.drawcard(oldcat);
|
||||
r.drawcard(newcat);
|
||||
if (r.is_act(newcat)) {
|
||||
r.tail = Math.max(r.tail, nfile + 1);
|
||||
if (!ebi('f' + nfile))
|
||||
this.addrow(nfile);
|
||||
r.addrow(nfile);
|
||||
}
|
||||
else if (this.is_act(oldcat)) {
|
||||
while (this.head < Math.min(this.tab.length, this.tail) && this.precard[this.tab[this.head].in])
|
||||
this.head++;
|
||||
else if (r.is_act(oldcat)) {
|
||||
while (r.head < Math.min(r.tab.length, r.tail) && r.precard[r.tab[r.head].in])
|
||||
r.head++;
|
||||
|
||||
if (!bz_act) {
|
||||
var tr = ebi("f" + nfile);
|
||||
@@ -328,10 +327,10 @@ function U2pvis(act, btns) {
|
||||
else return;
|
||||
|
||||
if (bz_act)
|
||||
this.bzw();
|
||||
r.bzw();
|
||||
};
|
||||
|
||||
this.bzw = function () {
|
||||
r.bzw = function () {
|
||||
var first = QS('#u2tab>tbody>tr:first-child');
|
||||
if (!first)
|
||||
return;
|
||||
@@ -340,93 +339,93 @@ function U2pvis(act, btns) {
|
||||
first = parseInt(first.getAttribute('id').slice(1));
|
||||
last = parseInt(last.getAttribute('id').slice(1));
|
||||
|
||||
while (this.head - first > this.wsz) {
|
||||
while (r.head - first > r.wsz) {
|
||||
var obj = ebi('f' + (first++));
|
||||
if (obj)
|
||||
obj.parentNode.removeChild(obj);
|
||||
}
|
||||
while (last - this.tail < this.wsz && last < this.tab.length - 2) {
|
||||
while (last - r.tail < r.wsz && last < r.tab.length - 2) {
|
||||
var obj = ebi('f' + (++last));
|
||||
if (!obj)
|
||||
this.addrow(last);
|
||||
r.addrow(last);
|
||||
}
|
||||
};
|
||||
|
||||
this.drawcard = function (cat) {
|
||||
r.drawcard = function (cat) {
|
||||
var cards = QSA('#u2cards>a>span');
|
||||
|
||||
if (cat == "q") {
|
||||
cards[4].innerHTML = this.ctr[cat];
|
||||
cards[4].innerHTML = r.ctr[cat];
|
||||
return;
|
||||
}
|
||||
if (cat == "bz") {
|
||||
cards[3].innerHTML = this.ctr[cat];
|
||||
cards[3].innerHTML = r.ctr[cat];
|
||||
return;
|
||||
}
|
||||
|
||||
cards[2].innerHTML = this.ctr["ok"] + this.ctr["ng"];
|
||||
cards[2].innerHTML = r.ctr["ok"] + r.ctr["ng"];
|
||||
|
||||
if (cat == "ng") {
|
||||
cards[1].innerHTML = this.ctr[cat];
|
||||
cards[1].innerHTML = r.ctr[cat];
|
||||
}
|
||||
if (cat == "ok") {
|
||||
cards[0].innerHTML = this.ctr[cat];
|
||||
cards[0].innerHTML = r.ctr[cat];
|
||||
}
|
||||
};
|
||||
|
||||
this.changecard = function (card) {
|
||||
this.act = card;
|
||||
this.precard = has(["ok", "ng", "done"], this.act) ? {} : this.act == "bz" ? { "ok": 1, "ng": 1 } : { "ok": 1, "ng": 1, "bz": 1 };
|
||||
this.postcard = has(["ok", "ng", "done"], this.act) ? { "bz": 1, "q": 1 } : this.act == "bz" ? { "q": 1 } : {};
|
||||
this.head = -1;
|
||||
this.tail = -1;
|
||||
r.changecard = function (card) {
|
||||
r.act = card;
|
||||
r.precard = has(["ok", "ng", "done"], r.act) ? {} : r.act == "bz" ? { "ok": 1, "ng": 1 } : { "ok": 1, "ng": 1, "bz": 1 };
|
||||
r.postcard = has(["ok", "ng", "done"], r.act) ? { "bz": 1, "q": 1 } : r.act == "bz" ? { "q": 1 } : {};
|
||||
r.head = -1;
|
||||
r.tail = -1;
|
||||
var html = [];
|
||||
for (var a = 0; a < this.tab.length; a++) {
|
||||
var rt = this.tab[a].in;
|
||||
if (this.is_act(rt)) {
|
||||
html.push(this.genrow(a, true));
|
||||
for (var a = 0; a < r.tab.length; a++) {
|
||||
var rt = r.tab[a].in;
|
||||
if (r.is_act(rt)) {
|
||||
html.push(r.genrow(a, true));
|
||||
|
||||
this.tail = a;
|
||||
if (this.head == -1)
|
||||
this.head = a;
|
||||
r.tail = a;
|
||||
if (r.head == -1)
|
||||
r.head = a;
|
||||
}
|
||||
}
|
||||
if (this.head == -1) {
|
||||
for (var a = 0; a < this.tab.length; a++) {
|
||||
var rt = this.tab[a].in;
|
||||
if (this.precard[rt]) {
|
||||
this.head = a + 1;
|
||||
this.tail = a;
|
||||
if (r.head == -1) {
|
||||
for (var a = 0; a < r.tab.length; a++) {
|
||||
var rt = r.tab[a].in;
|
||||
if (r.precard[rt]) {
|
||||
r.head = a + 1;
|
||||
r.tail = a;
|
||||
}
|
||||
else if (this.postcard[rt]) {
|
||||
this.head = a;
|
||||
this.tail = a - 1;
|
||||
else if (r.postcard[rt]) {
|
||||
r.head = a;
|
||||
r.tail = a - 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.head < 0)
|
||||
this.head = 0;
|
||||
if (r.head < 0)
|
||||
r.head = 0;
|
||||
|
||||
if (card == "bz") {
|
||||
for (var a = this.head - 1; a >= this.head - this.wsz && a >= 0; a--) {
|
||||
html.unshift(this.genrow(a, true).replace(/><td>/, "><td>a "));
|
||||
for (var a = r.head - 1; a >= r.head - r.wsz && a >= 0; a--) {
|
||||
html.unshift(r.genrow(a, true).replace(/><td>/, "><td>a "));
|
||||
}
|
||||
for (var a = this.tail + 1; a <= this.tail + this.wsz && a < this.tab.length; a++) {
|
||||
html.push(this.genrow(a, true).replace(/><td>/, "><td>b "));
|
||||
for (var a = r.tail + 1; a <= r.tail + r.wsz && a < r.tab.length; a++) {
|
||||
html.push(r.genrow(a, true).replace(/><td>/, "><td>b "));
|
||||
}
|
||||
}
|
||||
ebi('u2tab').tBodies[0].innerHTML = html.join('\n');
|
||||
};
|
||||
|
||||
this.genrow = function (nfile, as_html) {
|
||||
var r = this.tab[nfile],
|
||||
r.genrow = function (nfile, as_html) {
|
||||
var row = r.tab[nfile],
|
||||
td1 = '<td id="f' + nfile,
|
||||
td = '</td>' + td1,
|
||||
ret = td1 + 'n">' + r.hn +
|
||||
td + 't">' + r.ht +
|
||||
td + 'p" class="prog">' + r.hp + '</td>';
|
||||
ret = td1 + 'n">' + row.hn +
|
||||
td + 't">' + row.ht +
|
||||
td + 'p" class="prog">' + row.hp + '</td>';
|
||||
|
||||
if (as_html)
|
||||
return '<tr id="f' + nfile + '">' + ret + '</tr>';
|
||||
@@ -437,40 +436,50 @@ function U2pvis(act, btns) {
|
||||
return obj;
|
||||
};
|
||||
|
||||
this.addrow = function (nfile) {
|
||||
var tr = this.genrow(nfile);
|
||||
r.addrow = function (nfile) {
|
||||
var tr = r.genrow(nfile);
|
||||
ebi('u2tab').tBodies[0].appendChild(tr);
|
||||
};
|
||||
|
||||
var that = this;
|
||||
btns = QSA(btns + '>a[act]');
|
||||
for (var a = 0; a < btns.length; a++) {
|
||||
btns[a].onclick = function (e) {
|
||||
ev(e);
|
||||
var newtab = this.getAttribute('act');
|
||||
for (var b = 0; b < btns.length; b++) {
|
||||
btns[b].className = (
|
||||
btns[b].getAttribute('act') == newtab) ? 'act' : '';
|
||||
function go() {
|
||||
for (var b = 0; b < btns.length; b++) {
|
||||
btns[b].className = (
|
||||
btns[b].getAttribute('act') == newtab) ? 'act' : '';
|
||||
}
|
||||
r.changecard(newtab);
|
||||
}
|
||||
that.changecard(newtab);
|
||||
var nf = r.ctr[newtab];
|
||||
if (nf === undefined)
|
||||
nf = r.ctr["ok"] + r.ctr["ng"];
|
||||
|
||||
if (nf < 9000)
|
||||
return go();
|
||||
|
||||
modal.confirm('about to show ' + nf + ' files\n\nthis may crash your browser, are you sure?', go, null);
|
||||
};
|
||||
}
|
||||
|
||||
this.changecard(this.act);
|
||||
r.changecard(r.act);
|
||||
}
|
||||
|
||||
|
||||
function fsearch_explain(e) {
|
||||
ev(e);
|
||||
if (!has(perms, 'write'))
|
||||
return alert('your access to this folder is Read-Only\n\n' + (acct == '*' ? 'you are currently not logged in' : 'you are currently logged in as ' + acct));
|
||||
function fsearch_explain(n) {
|
||||
if (n)
|
||||
return toast.inf(60, 'your access to this folder is Read-Only\n\n' + (acct == '*' ? 'you are currently not logged in' : 'you are currently logged in as "' + acct + '"'));
|
||||
|
||||
alert('you are currently in file-search mode\n\nswitch to upload-mode by clicking the green magnifying glass (next to the big yellow search button), and then refresh\n\nsorry');
|
||||
if (bcfg_get('fsearch', false))
|
||||
return toast.inf(60, 'you are currently in file-search mode\n\nswitch to upload-mode by clicking the green magnifying glass (next to the big yellow search button), and then refresh\n\nsorry');
|
||||
|
||||
return toast.inf(60, 'refresh the page and try again, it should work now');
|
||||
}
|
||||
|
||||
|
||||
function up2k_init(subtle) {
|
||||
// show modal message
|
||||
function showmodal(msg) {
|
||||
ebi('u2notbtn').innerHTML = msg;
|
||||
ebi('u2btn').style.display = 'none';
|
||||
@@ -478,7 +487,6 @@ function up2k_init(subtle) {
|
||||
ebi('u2conf').style.opacity = '0.5';
|
||||
}
|
||||
|
||||
// hide modal message
|
||||
function unmodal() {
|
||||
ebi('u2notbtn').style.display = 'none';
|
||||
ebi('u2btn').style.display = 'block';
|
||||
@@ -495,7 +503,6 @@ function up2k_init(subtle) {
|
||||
// chrome<37 firefox<34 edge<12 opera<24 safari<7
|
||||
shame = 'your browser is impressively ancient';
|
||||
|
||||
// upload ui hidden by default, clicking the header shows it
|
||||
var got_deps = false;
|
||||
function init_deps() {
|
||||
if (!got_deps && !subtle && !window.asmCrypto) {
|
||||
@@ -512,11 +519,9 @@ function up2k_init(subtle) {
|
||||
got_deps = true;
|
||||
}
|
||||
|
||||
// show uploader if the user only has write-access
|
||||
if (perms.length && !has(perms, 'read'))
|
||||
goto('up2k');
|
||||
|
||||
// shows or clears a message in the basic uploader ui
|
||||
function setmsg(msg, type) {
|
||||
if (msg !== undefined) {
|
||||
ebi('u2err').setAttribute('class', type);
|
||||
@@ -534,13 +539,11 @@ function up2k_init(subtle) {
|
||||
}
|
||||
}
|
||||
|
||||
// switches to the basic uploader with msg as error message
|
||||
function un2k(msg) {
|
||||
setmsg(msg, 'err');
|
||||
return false;
|
||||
}
|
||||
|
||||
// handle user intent to use the basic uploader instead
|
||||
ebi('u2nope').onclick = function (e) {
|
||||
ev(e);
|
||||
setmsg(suggest_up2k, 'msg');
|
||||
@@ -642,7 +645,7 @@ function up2k_init(subtle) {
|
||||
else files = e.target.files;
|
||||
|
||||
if (!files || !files.length)
|
||||
return alert('no files selected??');
|
||||
return toast.err(0, 'no files selected??');
|
||||
|
||||
more_one_file();
|
||||
var bad_files = [],
|
||||
@@ -710,9 +713,9 @@ function up2k_init(subtle) {
|
||||
for (var a = 0; a < Math.min(20, missing.length); a++)
|
||||
msg.push(missing[a]);
|
||||
|
||||
alert(msg.join('\n-- '));
|
||||
dirs = [];
|
||||
pf = [];
|
||||
return modal.alert(msg.join('\n-- '), function () {
|
||||
read_dirs(rd, [], [], good, bad, spins);
|
||||
});
|
||||
}
|
||||
spins = 0;
|
||||
}
|
||||
@@ -757,7 +760,6 @@ function up2k_init(subtle) {
|
||||
}
|
||||
ngot += 1;
|
||||
});
|
||||
// console.log("ngot: " + ngot);
|
||||
if (!ngot) {
|
||||
dirs.shift();
|
||||
rd = null;
|
||||
@@ -777,16 +779,22 @@ function up2k_init(subtle) {
|
||||
if (good_files.length - bad_files.length <= 1 && ANDROID)
|
||||
msg += '\nFirefox-Android has a bug which prevents selecting multiple files. Try selecting one file at a time. For more info, see firefox bug 1456557';
|
||||
|
||||
alert(msg);
|
||||
return modal.alert(msg, function () {
|
||||
gotallfiles(good_files, []);
|
||||
});
|
||||
}
|
||||
|
||||
var msg = ['upload these ' + good_files.length + ' files?'];
|
||||
for (var a = 0, aa = Math.min(20, good_files.length); a < aa; a++)
|
||||
msg.push(good_files[a][1]);
|
||||
|
||||
if (ask_up && !fsearch && !confirm(msg.join('\n')))
|
||||
return;
|
||||
if (ask_up && !fsearch)
|
||||
return modal.confirm(msg.join('\n'), function () { up_them(good_files); }, null);
|
||||
|
||||
up_them(good_files);
|
||||
}
|
||||
|
||||
function up_them(good_files) {
|
||||
var seen = {},
|
||||
evpath = get_evpath(),
|
||||
draw_each = good_files.length < 50;
|
||||
@@ -796,17 +804,25 @@ function up2k_init(subtle) {
|
||||
|
||||
for (var a = 0; a < good_files.length; a++) {
|
||||
var fobj = good_files[a][0],
|
||||
name = good_files[a][1],
|
||||
fdir = '',
|
||||
now = Date.now(),
|
||||
lmod = fobj.lastModified || now;
|
||||
lmod = fobj.lastModified || now,
|
||||
ofs = name.lastIndexOf('/') + 1;
|
||||
|
||||
if (ofs) {
|
||||
fdir = name.slice(0, ofs);
|
||||
name = name.slice(ofs);
|
||||
}
|
||||
|
||||
var entry = {
|
||||
"n": st.files.length,
|
||||
"t0": now,
|
||||
"fobj": fobj,
|
||||
"name": good_files[a][1],
|
||||
"name": name,
|
||||
"size": fobj.size,
|
||||
"lmod": lmod / 1000,
|
||||
"purl": evpath,
|
||||
"purl": fdir,
|
||||
"done": false,
|
||||
"hash": []
|
||||
},
|
||||
@@ -907,7 +923,9 @@ function up2k_init(subtle) {
|
||||
}
|
||||
return handshakes_permitted() && 0 ==
|
||||
st.todo.handshake.length +
|
||||
st.busy.handshake.length;
|
||||
st.busy.handshake.length +
|
||||
st.todo.upload.length +
|
||||
st.busy.upload.length;
|
||||
}
|
||||
|
||||
var tasker = (function () {
|
||||
@@ -1147,7 +1165,7 @@ function up2k_init(subtle) {
|
||||
return tasker();
|
||||
}
|
||||
|
||||
alert('y o u b r o k e i t\nfile: ' + t.name + '\nerror: ' + err);
|
||||
toast.err(0, 'y o u b r o k e i t\nfile: ' + t.name + '\nerror: ' + err);
|
||||
};
|
||||
reader.readAsArrayBuffer(
|
||||
bobslice.call(t.fobj, car, cdr));
|
||||
@@ -1301,7 +1319,8 @@ function up2k_init(subtle) {
|
||||
|
||||
if (!response || !response.hits || !response.hits.length) {
|
||||
smsg = '404';
|
||||
msg = 'not found on server <a href="#" onclick="fsearch_explain()" class="fsearch_explain">(explain)</a>';
|
||||
msg = ('not found on server <a href="#" onclick="fsearch_explain(' +
|
||||
(has(perms, 'write') ? '0' : '1') + ')" class="fsearch_explain">(explain)</a>');
|
||||
}
|
||||
else {
|
||||
smsg = 'found';
|
||||
@@ -1325,9 +1344,10 @@ function up2k_init(subtle) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.name !== t.name) {
|
||||
// file exists; server renamed us
|
||||
console.log("server-rename [" + t.name + "] to [" + response.name + "]");
|
||||
if (response.purl !== t.purl || response.name !== t.name) {
|
||||
// server renamed us (file exists / path restrictions)
|
||||
console.log("server-rename [" + t.purl + "] [" + t.name + "] to [" + response.purl + "] [" + response.name + "]");
|
||||
t.purl = response.purl;
|
||||
t.name = response.name;
|
||||
pvis.seth(t.n, 0, linksplit(t.purl + t.name).join(' '));
|
||||
}
|
||||
@@ -1347,7 +1367,7 @@ function up2k_init(subtle) {
|
||||
for (var a = 0; a < missing.length; a++) {
|
||||
var idx = t.hash.indexOf(missing[a]);
|
||||
if (idx < 0)
|
||||
return alert('wtf negative index for hash "{0}" in task:\n{1}'.format(
|
||||
return modal.alert('wtf negative index for hash "{0}" in task:\n{1}'.format(
|
||||
missing[a], JSON.stringify(t)));
|
||||
|
||||
t.postlist.push(idx);
|
||||
@@ -1436,7 +1456,7 @@ function up2k_init(subtle) {
|
||||
tasker();
|
||||
return;
|
||||
}
|
||||
alert("server broke; hs-err {0} on file [{1}]:\n".format(
|
||||
toast.err(0, "server broke; hs-err {0} on file [{1}]:\n".format(
|
||||
xhr.status, t.name) + (
|
||||
(xhr.response && xhr.response.err) ||
|
||||
(xhr.responseText && xhr.responseText) ||
|
||||
@@ -1456,7 +1476,7 @@ function up2k_init(subtle) {
|
||||
if (fsearch)
|
||||
req.srch = 1;
|
||||
|
||||
xhr.open('POST', t.purl + 'handshake.php', true);
|
||||
xhr.open('POST', t.purl, true);
|
||||
xhr.responseType = 'text';
|
||||
xhr.send(JSON.stringify(req));
|
||||
}
|
||||
@@ -1496,7 +1516,7 @@ function up2k_init(subtle) {
|
||||
console.log("ignoring dupe-segment error", t);
|
||||
}
|
||||
else {
|
||||
alert("server broke; cu-err {0} on file [{1}]:\n".format(
|
||||
toast.err(0, "server broke; cu-err {0} on file [{1}]:\n".format(
|
||||
xhr.status, t.name) + (txt || "no further information"));
|
||||
return;
|
||||
}
|
||||
@@ -1524,7 +1544,7 @@ function up2k_init(subtle) {
|
||||
console.log('chunkpit onerror, retrying', t);
|
||||
do_send();
|
||||
};
|
||||
xhr.open('POST', t.purl + 'chunkpit.php', true);
|
||||
xhr.open('POST', t.purl, true);
|
||||
xhr.setRequestHeader("X-Up2k-Hash", t.hash[npart]);
|
||||
xhr.setRequestHeader("X-Up2k-Wark", t.wark);
|
||||
xhr.setRequestHeader('Content-Type', 'application/octet-stream');
|
||||
@@ -1544,7 +1564,7 @@ function up2k_init(subtle) {
|
||||
|
||||
function onresize(e) {
|
||||
var bar = ebi('ops'),
|
||||
wpx = innerWidth,
|
||||
wpx = window.innerWidth,
|
||||
fpx = parseInt(getComputedStyle(bar)['font-size']),
|
||||
wem = wpx * 1.0 / fpx,
|
||||
wide = wem > 54,
|
||||
|
||||
@@ -1,302 +0,0 @@
|
||||
|
||||
#op_up2k {
|
||||
padding: 0 1em 1em 1em;
|
||||
}
|
||||
#u2form {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 2px;
|
||||
height: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
#u2form input {
|
||||
background: #444;
|
||||
border: 0px solid #444;
|
||||
outline: none;
|
||||
}
|
||||
#u2err.err {
|
||||
color: #f87;
|
||||
padding: .5em;
|
||||
}
|
||||
#u2err.msg {
|
||||
color: #999;
|
||||
padding: .5em;
|
||||
font-size: .9em;
|
||||
}
|
||||
#u2btn {
|
||||
color: #eee;
|
||||
background: #555;
|
||||
background: -moz-linear-gradient(top, #367 0%, #489 50%, #38788a 51%, #367 100%);
|
||||
background: -webkit-linear-gradient(top, #367 0%, #489 50%, #38788a 51%, #367 100%);
|
||||
background: linear-gradient(to bottom, #367 0%, #489 50%, #38788a 51%, #367 100%);
|
||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#489', endColorstr='#38788a', GradientType=0);
|
||||
text-decoration: none;
|
||||
line-height: 1.3em;
|
||||
border: 1px solid #222;
|
||||
border-radius: .4em;
|
||||
text-align: center;
|
||||
font-size: 1.5em;
|
||||
margin: .5em auto;
|
||||
padding: .8em 0;
|
||||
width: 16em;
|
||||
cursor: pointer;
|
||||
box-shadow: .4em .4em 0 #111;
|
||||
}
|
||||
#op_up2k.srch #u2btn {
|
||||
background: linear-gradient(to bottom, #ca3 0%, #fd8 50%, #fc6 51%, #b92 100%);
|
||||
text-shadow: 1px 1px 1px #fc6;
|
||||
color: #333;
|
||||
}
|
||||
#u2conf #u2btn {
|
||||
margin: -1.5em 0;
|
||||
padding: .8em 0;
|
||||
width: 100%;
|
||||
max-width: 12em;
|
||||
display: inline-block;
|
||||
}
|
||||
#u2conf #u2btn_cw {
|
||||
text-align: right;
|
||||
}
|
||||
#u2notbtn {
|
||||
display: none;
|
||||
text-align: center;
|
||||
background: #333;
|
||||
padding-top: 1em;
|
||||
}
|
||||
#u2notbtn * {
|
||||
line-height: 1.3em;
|
||||
}
|
||||
#u2tab {
|
||||
margin: 3em auto;
|
||||
width: calc(100% - 2em);
|
||||
max-width: 100em;
|
||||
}
|
||||
#op_up2k.srch #u2tab {
|
||||
max-width: none;
|
||||
}
|
||||
#u2tab td {
|
||||
border: 1px solid #ccc;
|
||||
border-width: 0 0px 1px 0;
|
||||
padding: .1em .3em;
|
||||
}
|
||||
#u2tab td:nth-child(2) {
|
||||
width: 5em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
#u2tab td:nth-child(3) {
|
||||
width: 40%;
|
||||
}
|
||||
#op_up2k.srch td.prog {
|
||||
font-family: sans-serif;
|
||||
font-size: 1em;
|
||||
width: auto;
|
||||
}
|
||||
#u2tab tbody tr:hover td {
|
||||
background: #222;
|
||||
}
|
||||
#u2cards {
|
||||
padding: 1em 0 .3em 1em;
|
||||
margin: 1.5em auto -2.5em auto;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
#u2cards.w {
|
||||
width: 45em;
|
||||
text-align: left;
|
||||
}
|
||||
#u2cards a {
|
||||
padding: .2em 1em;
|
||||
border: 1px solid #777;
|
||||
border-width: 0 0 1px 0;
|
||||
background: linear-gradient(to bottom, #333, #222);
|
||||
}
|
||||
#u2cards a:first-child {
|
||||
border-radius: .4em 0 0 0;
|
||||
}
|
||||
#u2cards a:last-child {
|
||||
border-radius: 0 .4em 0 0;
|
||||
}
|
||||
#u2cards a.act {
|
||||
padding-bottom: .5em;
|
||||
border-width: 1px 1px .1em 1px;
|
||||
border-radius: .3em .3em 0 0;
|
||||
margin-left: -1px;
|
||||
background: linear-gradient(to bottom, #464, #333 80%);
|
||||
box-shadow: 0 -.17em .67em #280;
|
||||
border-color: #7c5 #583 #333 #583;
|
||||
position: relative;
|
||||
color: #fd7;
|
||||
}
|
||||
#u2cards span {
|
||||
color: #fff;
|
||||
}
|
||||
#u2conf {
|
||||
margin: 1em auto;
|
||||
width: 30em;
|
||||
}
|
||||
#u2conf.has_btn {
|
||||
width: 48em;
|
||||
}
|
||||
#u2conf * {
|
||||
text-align: center;
|
||||
line-height: 1em;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
#u2conf .txtbox {
|
||||
width: 3em;
|
||||
color: #fff;
|
||||
background: #444;
|
||||
border: 1px solid #777;
|
||||
font-size: 1.2em;
|
||||
padding: .15em 0;
|
||||
height: 1.05em;
|
||||
}
|
||||
#u2conf .txtbox.err {
|
||||
background: #922;
|
||||
}
|
||||
#u2conf a {
|
||||
color: #fff;
|
||||
background: #c38;
|
||||
text-decoration: none;
|
||||
border-radius: .1em;
|
||||
font-size: 1.5em;
|
||||
padding: .1em 0;
|
||||
margin: 0 -1px;
|
||||
width: 1.5em;
|
||||
height: 1em;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
bottom: -0.08em;
|
||||
}
|
||||
#u2conf input+a {
|
||||
background: #d80;
|
||||
}
|
||||
#u2conf label {
|
||||
font-size: 1.6em;
|
||||
width: 2em;
|
||||
height: 1em;
|
||||
padding: .4em 0;
|
||||
display: block;
|
||||
border-radius: .25em;
|
||||
}
|
||||
#u2conf input[type="checkbox"] {
|
||||
position: relative;
|
||||
opacity: .02;
|
||||
top: 2em;
|
||||
}
|
||||
#u2conf input[type="checkbox"]+label {
|
||||
position: relative;
|
||||
background: #603;
|
||||
border-bottom: .2em solid #a16;
|
||||
box-shadow: 0 .1em .3em #a00 inset;
|
||||
}
|
||||
#u2conf input[type="checkbox"]:checked+label {
|
||||
background: #6a1;
|
||||
border-bottom: .2em solid #efa;
|
||||
box-shadow: 0 .1em .5em #0c0;
|
||||
}
|
||||
#u2conf input[type="checkbox"]+label:hover {
|
||||
box-shadow: 0 .1em .3em #fb0;
|
||||
border-color: #fb0;
|
||||
}
|
||||
#op_up2k.srch #u2conf td:nth-child(1)>*,
|
||||
#op_up2k.srch #u2conf td:nth-child(2)>*,
|
||||
#op_up2k.srch #u2conf td:nth-child(3)>* {
|
||||
background: #777;
|
||||
border-color: #ccc;
|
||||
box-shadow: none;
|
||||
opacity: .2;
|
||||
}
|
||||
#u2foot {
|
||||
color: #fff;
|
||||
font-style: italic;
|
||||
}
|
||||
#u2foot .warn {
|
||||
font-size: 1.3em;
|
||||
padding: .5em .8em;
|
||||
margin: 1em -.6em;
|
||||
color: #f74;
|
||||
background: #322;
|
||||
border: 1px solid #633;
|
||||
border-width: .1em 0;
|
||||
text-align: center;
|
||||
}
|
||||
#u2foot .warn span {
|
||||
color: #f86;
|
||||
}
|
||||
html.light #u2foot .warn {
|
||||
color: #b00;
|
||||
background: #fca;
|
||||
border-color: #f70;
|
||||
}
|
||||
html.light #u2foot .warn span {
|
||||
color: #930;
|
||||
}
|
||||
#u2foot span {
|
||||
color: #999;
|
||||
font-size: .9em;
|
||||
font-weight: normal;
|
||||
}
|
||||
#u2footfoot {
|
||||
margin-bottom: -1em;
|
||||
}
|
||||
.prog {
|
||||
font-family: monospace, monospace;
|
||||
}
|
||||
#u2tab a>span {
|
||||
font-weight: bold;
|
||||
font-style: italic;
|
||||
color: #fff;
|
||||
padding-left: .2em;
|
||||
}
|
||||
#u2cleanup {
|
||||
float: right;
|
||||
margin-bottom: -.3em;
|
||||
}
|
||||
.fsearch_explain {
|
||||
padding-left: .7em;
|
||||
font-size: 1.1em;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
html.light #u2btn {
|
||||
box-shadow: .4em .4em 0 #ccc;
|
||||
}
|
||||
html.light #u2cards span {
|
||||
color: #000;
|
||||
}
|
||||
html.light #u2cards a {
|
||||
background: linear-gradient(to bottom, #eee, #fff);
|
||||
}
|
||||
html.light #u2cards a.act {
|
||||
color: #037;
|
||||
background: inherit;
|
||||
box-shadow: 0 -.17em .67em #0ad;
|
||||
border-color: #09c #05a #eee #05a;
|
||||
}
|
||||
html.light #u2conf .txtbox {
|
||||
background: #fff;
|
||||
color: #444;
|
||||
}
|
||||
html.light #u2conf .txtbox.err {
|
||||
background: #f96;
|
||||
color: #300;
|
||||
}
|
||||
html.light #op_up2k.srch #u2btn {
|
||||
border-color: #a80;
|
||||
}
|
||||
html.light #u2foot {
|
||||
color: #000;
|
||||
}
|
||||
html.light #u2tab tbody tr:hover td {
|
||||
background: #fff;
|
||||
}
|
||||
@@ -140,10 +140,10 @@ function import_js(url, cb) {
|
||||
var script = mknod('script');
|
||||
script.type = 'text/javascript';
|
||||
script.src = url;
|
||||
|
||||
script.onreadystatechange = cb;
|
||||
script.onload = cb;
|
||||
|
||||
script.onerror = function () {
|
||||
toast.err(0, 'Failed to load module:\n' + url);
|
||||
};
|
||||
head.appendChild(script);
|
||||
}
|
||||
|
||||
@@ -656,14 +656,24 @@ var tt = (function () {
|
||||
})();
|
||||
|
||||
|
||||
function lf2br(txt) {
|
||||
var html = '', hp = txt.split(/(?=<.?pre>)/i);
|
||||
for (var a = 0; a < hp.length; a++)
|
||||
html += hp[a].startsWith('<pre>') ? hp[a] :
|
||||
hp[a].replace(/<br ?.?>\n/g, '\n').replace(/\n<br ?.?>/g, '\n').replace(/\n/g, '<br />\n');
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
|
||||
var toast = (function () {
|
||||
var r = {},
|
||||
te = null,
|
||||
visible = false,
|
||||
obj = mknod('div');
|
||||
|
||||
obj.setAttribute('id', 'toast');
|
||||
document.body.appendChild(obj);;
|
||||
document.body.appendChild(obj);
|
||||
r.visible = false;
|
||||
|
||||
r.hide = function (e) {
|
||||
ev(e);
|
||||
@@ -677,12 +687,7 @@ var toast = (function () {
|
||||
if (ms)
|
||||
te = setTimeout(r.hide, ms * 1000);
|
||||
|
||||
var html = '', hp = txt.split(/(?=<.?pre>)/i);
|
||||
for (var a = 0; a < hp.length; a++)
|
||||
html += hp[a].startsWith('<pre>') ? hp[a] :
|
||||
hp[a].replace(/<br ?.?>\n/g, '\n').replace(/\n<br ?.?>/g, '\n').replace(/\n/g, '<br />\n');
|
||||
|
||||
obj.innerHTML = '<a href="#" id="toastc">x</a>' + html;
|
||||
obj.innerHTML = '<a href="#" id="toastc">x</a>' + lf2br(txt);
|
||||
obj.className = cl;
|
||||
ms += obj.offsetWidth;
|
||||
obj.className += ' vis';
|
||||
@@ -705,3 +710,113 @@ var toast = (function () {
|
||||
|
||||
return r;
|
||||
})();
|
||||
|
||||
|
||||
var modal = (function () {
|
||||
var r = {},
|
||||
q = [],
|
||||
o = null,
|
||||
cb_ok = null,
|
||||
cb_ng = null;
|
||||
|
||||
r.busy = false;
|
||||
|
||||
r.show = function (html) {
|
||||
o = mknod('div');
|
||||
o.setAttribute('id', 'modal');
|
||||
o.innerHTML = '<table><tr><td><div id="modalc">' + html + '</div></td></tr></table>';
|
||||
document.body.appendChild(o);
|
||||
document.addEventListener('keydown', onkey);
|
||||
r.busy = true;
|
||||
|
||||
var a = ebi('modal-ng');
|
||||
if (a)
|
||||
a.onclick = ng;
|
||||
|
||||
a = ebi('modal-ok');
|
||||
a.onclick = ok;
|
||||
|
||||
(ebi('modali') || a).focus();
|
||||
};
|
||||
|
||||
r.hide = function () {
|
||||
o.parentNode.removeChild(o);
|
||||
document.removeEventListener('keydown', onkey);
|
||||
r.busy = false;
|
||||
setTimeout(next, 50);
|
||||
};
|
||||
function ok(e) {
|
||||
ev(e);
|
||||
var v = ebi('modali');
|
||||
v = v ? v.value : true;
|
||||
r.hide();
|
||||
if (cb_ok)
|
||||
cb_ok(v);
|
||||
}
|
||||
function ng(e) {
|
||||
ev(e);
|
||||
r.hide();
|
||||
if (cb_ng)
|
||||
cb_ng(null);
|
||||
}
|
||||
|
||||
function onkey(e) {
|
||||
if (e.code == 'Enter') {
|
||||
var a = ebi('modal-ng');
|
||||
if (a && document.activeElement == a)
|
||||
return ng();
|
||||
|
||||
return ok();
|
||||
}
|
||||
|
||||
if (e.code == 'Escape')
|
||||
return ng();
|
||||
}
|
||||
|
||||
function next() {
|
||||
if (!r.busy && q.length)
|
||||
q.shift()();
|
||||
}
|
||||
|
||||
r.alert = function (html, cb) {
|
||||
q.push(function () {
|
||||
_alert(lf2br(html), cb);
|
||||
});
|
||||
next();
|
||||
};
|
||||
function _alert(html, cb) {
|
||||
cb_ok = cb_ng = cb;
|
||||
html += '<div id="modalb"><a href="#" id="modal-ok">OK</a></div>';
|
||||
r.show(html);
|
||||
}
|
||||
|
||||
r.confirm = function (html, cok, cng) {
|
||||
q.push(function () {
|
||||
_confirm(lf2br(html), cok, cng);
|
||||
});
|
||||
next();
|
||||
}
|
||||
function _confirm(html, cok, cng) {
|
||||
cb_ok = cok;
|
||||
cb_ng = cng === undefined ? cok : null;
|
||||
html += '<div id="modalb"><a href="#" id="modal-ok">OK</a><a href="#" id="modal-ng">Cancel</a></div>';
|
||||
r.show(html);
|
||||
}
|
||||
|
||||
r.prompt = function (html, v, cok, cng) {
|
||||
q.push(function () {
|
||||
_prompt(lf2br(html), v, cok, cng);
|
||||
});
|
||||
next();
|
||||
}
|
||||
function _prompt(html, v, cok, cng) {
|
||||
cb_ok = cok;
|
||||
cb_ng = cng === undefined ? cok : null;
|
||||
html += '<input id="modali" type="text" /><div id="modalb"><a href="#" id="modal-ok">OK</a><a href="#" id="modal-ng">Cancel</a></div>';
|
||||
r.show(html);
|
||||
|
||||
ebi('modali').value = v || '';
|
||||
}
|
||||
|
||||
return r;
|
||||
})();
|
||||
|
||||
@@ -239,7 +239,8 @@ awk '/^LOREM_IPSUM_WORDS/{o=1;print "LOREM_IPSUM_WORDS = u\"a\"";next} !o; /"""/
|
||||
tmv "$f"
|
||||
|
||||
# up2k goes from 28k to 22k laff
|
||||
echo entabbening
|
||||
awk 'BEGIN{gensub(//,"",1)}' </dev/null &&
|
||||
echo entabbening &&
|
||||
find | grep -E '\.css$' | while IFS= read -r f; do
|
||||
awk '{
|
||||
sub(/^[ \t]+/,"");
|
||||
@@ -253,6 +254,7 @@ find | grep -E '\.css$' | while IFS= read -r f; do
|
||||
' <$f | sed 's/;\}$/}/' >t
|
||||
tmv "$f"
|
||||
done
|
||||
unexpand -h 2>/dev/null &&
|
||||
find | grep -E '\.(js|html)$' | while IFS= read -r f; do
|
||||
unexpand -t 4 --first-only <"$f" >t
|
||||
tmv "$f"
|
||||
|
||||
@@ -364,7 +364,7 @@ def confirm(rv):
|
||||
except:
|
||||
pass
|
||||
|
||||
sys.exit(rv)
|
||||
sys.exit(rv or 1)
|
||||
|
||||
|
||||
def run(tmp, j2):
|
||||
|
||||
1
setup.py
1
setup.py
@@ -99,6 +99,7 @@ args = {
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: Implementation :: CPython",
|
||||
"Programming Language :: Python :: Implementation :: PyPy",
|
||||
"Environment :: Console",
|
||||
|
||||
@@ -39,6 +39,7 @@ class Cfg(Namespace):
|
||||
no_scandir=False,
|
||||
no_sendfile=True,
|
||||
no_rescan=True,
|
||||
re_maxage=0,
|
||||
ihead=False,
|
||||
nih=True,
|
||||
mtp=[],
|
||||
|
||||
@@ -26,6 +26,7 @@ class Cfg(Namespace):
|
||||
"no_hash": False,
|
||||
"css_browser": None,
|
||||
"no_voldump": True,
|
||||
"re_maxage": 0,
|
||||
"rproxy": 0,
|
||||
}
|
||||
ex.update(ex2)
|
||||
|
||||
Reference in New Issue
Block a user