Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
400d700845 | ||
|
|
82ce6862ee | ||
|
|
38e4fdfe03 | ||
|
|
c04662798d | ||
|
|
19d156ff4e | ||
|
|
87c60a1ec9 | ||
|
|
2c92dab165 | ||
|
|
5c1e23907d | ||
|
|
925c7f0a57 | ||
|
|
feed08deb2 | ||
|
|
560d7b6672 | ||
|
|
565daee98b | ||
|
|
e396c5c2b5 | ||
|
|
1ee2cdd089 | ||
|
|
beacedab50 | ||
|
|
25139a4358 | ||
|
|
f8491970fd | ||
|
|
da091aec85 | ||
|
|
e9eb5affcd | ||
|
|
c1918bc36c | ||
|
|
fdda567f50 | ||
|
|
603d0ed72b | ||
|
|
b15a4ef79f | ||
|
|
48a6789d36 | ||
|
|
36f2c446af | ||
|
|
69517e4624 | ||
|
|
ea270ab9f2 | ||
|
|
b6cf2d3089 | ||
|
|
e8db3dd37f | ||
|
|
27485a4cb1 | ||
|
|
253a414443 | ||
|
|
f6e693f0f5 | ||
|
|
c5f7cfc355 | ||
|
|
bc2c1e427a | ||
|
|
95d9e693c6 | ||
|
|
70a3cf36d1 | ||
|
|
aa45fccf11 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -12,6 +12,7 @@ copyparty.egg-info/
|
||||
/dist/
|
||||
/py2/
|
||||
/sfx*
|
||||
/pyz/
|
||||
/unt/
|
||||
/log/
|
||||
|
||||
|
||||
61
README.md
61
README.md
@@ -47,6 +47,7 @@ turn almost any device into a file server with resumable uploads/downloads using
|
||||
* [audio equalizer](#audio-equalizer) - and [dynamic range compressor](https://en.wikipedia.org/wiki/Dynamic_range_compression)
|
||||
* [fix unreliable playback on android](#fix-unreliable-playback-on-android) - due to phone / app settings
|
||||
* [markdown viewer](#markdown-viewer) - and there are *two* editors
|
||||
* [markdown vars](#markdown-vars) - dynamic docs with serverside variable expansion
|
||||
* [other tricks](#other-tricks)
|
||||
* [searching](#searching) - search by size, date, path/name, mp3-tags, ...
|
||||
* [server config](#server-config) - using arguments or config files, or a mix of both
|
||||
@@ -60,6 +61,7 @@ turn almost any device into a file server with resumable uploads/downloads using
|
||||
* [tftp server](#tftp-server) - a TFTP server (read/write) can be started using `--tftp 3969`
|
||||
* [smb server](#smb-server) - unsafe, slow, not recommended for wan
|
||||
* [browser ux](#browser-ux) - tweaking the ui
|
||||
* [opengraph](#opengraph) - discord and social-media embeds
|
||||
* [file indexing](#file-indexing) - enables dedup and music search ++
|
||||
* [exclude-patterns](#exclude-patterns) - to save some time
|
||||
* [filesystem guards](#filesystem-guards) - avoid traversing into other filesystems
|
||||
@@ -108,8 +110,9 @@ turn almost any device into a file server with resumable uploads/downloads using
|
||||
* [dependencies](#dependencies) - mandatory deps
|
||||
* [optional dependencies](#optional-dependencies) - install these to enable bonus features
|
||||
* [optional gpl stuff](#optional-gpl-stuff)
|
||||
* [sfx](#sfx) - the self-contained "binary"
|
||||
* [sfx](#sfx) - the self-contained "binary" (recommended!)
|
||||
* [copyparty.exe](#copypartyexe) - download [copyparty.exe](https://github.com/9001/copyparty/releases/latest/download/copyparty.exe) (win8+) or [copyparty32.exe](https://github.com/9001/copyparty/releases/latest/download/copyparty32.exe) (win7+)
|
||||
* [zipapp](#zipapp) - another emergency alternative, [copyparty.pyz](https://github.com/9001/copyparty/releases/latest/download/copyparty.pyz)
|
||||
* [install on android](#install-on-android)
|
||||
* [reporting bugs](#reporting-bugs) - ideas for context to include, and where to submit them
|
||||
* [devnotes](#devnotes) - for build instructions etc, see [./docs/devnotes.md](./docs/devnotes.md)
|
||||
@@ -123,6 +126,7 @@ just run **[copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/
|
||||
* or if you cannot install python, you can use [copyparty.exe](#copypartyexe) instead
|
||||
* or install [on arch](#arch-package) ╱ [on NixOS](#nixos-module) ╱ [through nix](#nix-package)
|
||||
* or if you are on android, [install copyparty in termux](#install-on-android)
|
||||
* or if your computer is messed up and nothing else works, [try the pyz](#zipapp)
|
||||
* or if you prefer to [use docker](./scripts/docker/) 🐋 you can do that too
|
||||
* docker has all deps built-in, so skip this step:
|
||||
|
||||
@@ -226,6 +230,7 @@ also see [comparison to similar software](./docs/versus.md)
|
||||
* ☑ ...of videos using FFmpeg
|
||||
* ☑ ...of audio (spectrograms) using FFmpeg
|
||||
* ☑ cache eviction (max-age; maybe max-size eventually)
|
||||
* ☑ multilingual UI (english, norwegian, [add your own](./docs/rice/#translations)))
|
||||
* ☑ SPA (browse while uploading)
|
||||
* server indexing
|
||||
* ☑ [locate files by contents](#file-search)
|
||||
@@ -234,9 +239,11 @@ also see [comparison to similar software](./docs/versus.md)
|
||||
* client support
|
||||
* ☑ [folder sync](#folder-sync)
|
||||
* ☑ [curl-friendly](https://user-images.githubusercontent.com/241032/215322619-ea5fd606-3654-40ad-94ee-2bc058647bb2.png)
|
||||
* ☑ [opengraph](#opengraph) (discord embeds)
|
||||
* markdown
|
||||
* ☑ [viewer](#markdown-viewer)
|
||||
* ☑ editor (sure why not)
|
||||
* ☑ [variables](#markdown-vars)
|
||||
|
||||
PS: something missing? post any crazy ideas you've got as a [feature request](https://github.com/9001/copyparty/issues/new?assignees=9001&labels=enhancement&template=feature_request.md) or [discussion](https://github.com/9001/copyparty/discussions/new?category=ideas) 🤙
|
||||
|
||||
@@ -406,7 +413,7 @@ configuring accounts/volumes with arguments:
|
||||
`-v .::r,usr1,usr2:rw,usr3,usr4` = usr1/2 read-only, 3/4 read-write
|
||||
|
||||
permissions:
|
||||
* `r` (read): browse folder contents, download files, download as zip/tar
|
||||
* `r` (read): browse folder contents, download files, download as zip/tar, see filekeys/dirkeys
|
||||
* `w` (write): upload files, move files *into* this folder
|
||||
* `m` (move): move files/folders *from* this folder
|
||||
* `d` (delete): delete files/folders
|
||||
@@ -608,15 +615,21 @@ you can also zip a selection of files or folders by clicking them in the browser
|
||||
|
||||
cool trick: download a folder by appending url-params `?tar&opus` or `?tar&mp3` to transcode all audio files (except aac|m4a|mp3|ogg|opus|wma) to opus/mp3 before they're added to the archive
|
||||
* super useful if you're 5 minutes away from takeoff and realize you don't have any music on your phone but your server only has flac files and downloading those will burn through all your data + there wouldn't be enough time anyways
|
||||
* and url-params `&j` / `&w` produce jpeg/webm thumbnails/spectrograms instead of the original audio/video/images
|
||||
* and url-params `&j` / `&w` produce jpeg/webm thumbnails/spectrograms instead of the original audio/video/images (`&p` for audio waveforms)
|
||||
* can also be used to pregenerate thumbnails; combine with `--th-maxage=9999999` or `--th-clean=0`
|
||||
|
||||
|
||||
## uploading
|
||||
|
||||
drag files/folders into the web-browser to upload (or use the [command-line uploader](https://github.com/9001/copyparty/tree/hovudstraum/bin#u2cpy))
|
||||
drag files/folders into the web-browser to upload
|
||||
|
||||
this initiates an upload using `up2k`; there are two uploaders available:
|
||||
dragdrop is the recommended way, but you may also:
|
||||
|
||||
* select some files (not folders) in your file explorer and press CTRL-V inside the browser window
|
||||
* use the [command-line uploader](https://github.com/9001/copyparty/tree/hovudstraum/bin#u2cpy)
|
||||
* upload using [curl or sharex](#client-examples)
|
||||
|
||||
when uploading files through dragdrop or CTRL-V, this initiates an upload using `up2k`; there are two browser-based uploaders available:
|
||||
* `[🎈] bup`, the basic uploader, supports almost every browser since netscape 4.0
|
||||
* `[🚀] up2k`, the good / fancy one
|
||||
|
||||
@@ -843,6 +856,13 @@ other notes,
|
||||
* the document preview has a max-width which is the same as an A4 paper when printed
|
||||
|
||||
|
||||
### markdown vars
|
||||
|
||||
dynamic docs with serverside variable expansion to replace stuff like `{{self.ip}}` with the client's IP, or `{{srv.htime}}` with the current time on the server
|
||||
|
||||
see [./srv/expand/](./srv/expand/) for usage and examples
|
||||
|
||||
|
||||
## other tricks
|
||||
|
||||
* you can link a particular timestamp in an audio file by adding it to the URL, such as `&20` / `&20s` / `&1m20` / `&t=1:20` after the `.../#af-c8960dab`
|
||||
@@ -891,6 +911,8 @@ using arguments or config files, or a mix of both:
|
||||
|
||||
**NB:** as humongous as this readme is, there is also a lot of undocumented features. Run copyparty with `--help` to see all available global options; all of those can be used in the `[global]` section of config files, and everything listed in `--help-flags` can be used in volumes as volflags.
|
||||
* if running in docker/podman, try this: `docker run --rm -it copyparty/ac --help`
|
||||
* or see this (probably outdated): https://ocv.me/copyparty/helptext.html
|
||||
* or if you prefer plaintext, https://ocv.me/copyparty/helptext.txt
|
||||
|
||||
|
||||
## zeroconf
|
||||
@@ -1067,7 +1089,22 @@ tweaking the ui
|
||||
* to sort in music order (album, track, artist, title) with filename as fallback, you could `--sort tags/Cirle,tags/.tn,tags/Artist,tags/Title,href`
|
||||
* to sort by upload date, first enable showing the upload date in the listing with `-e2d -mte +.up_at` and then `--sort tags/.up_at`
|
||||
|
||||
see [./docs/rice](./docs/rice) for more
|
||||
see [./docs/rice](./docs/rice) for more, including how to add stuff (css/`<meta>`/...) to the html `<head>` tag, or to add your own translation
|
||||
|
||||
|
||||
## opengraph
|
||||
|
||||
discord and social-media embeds
|
||||
|
||||
can be enabled globally with `--og` or per-volume with volflag `og`
|
||||
|
||||
note that this disables hotlinking because the opengraph spec demands it; to sneak past this intentional limitation, you can enable opengraph selectively by user-agent, for example `--og-ua '(Discord|Twitter|Slack)bot'` (or volflag `og_ua`)
|
||||
|
||||
you can also hotlink files regardless by appending `?raw` to the url
|
||||
|
||||
NOTE: because discord (and maybe others) strip query args such as `?raw` in opengraph tags, any links which require a filekey or dirkey will not work
|
||||
|
||||
if you want to entirely replace the copyparty response with your own jinja2 template, give the template filepath to `--og-tpl` or volflag `og_tpl` (all members of `HttpCli` are available through the `this` object)
|
||||
|
||||
|
||||
## file indexing
|
||||
@@ -1981,7 +2018,7 @@ these are standalone programs and will never be imported / evaluated by copypart
|
||||
|
||||
# sfx
|
||||
|
||||
the self-contained "binary" [copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py) will unpack itself and run copyparty, assuming you have python installed of course
|
||||
the self-contained "binary" (recommended!) [copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py) will unpack itself and run copyparty, assuming you have python installed of course
|
||||
|
||||
you can reduce the sfx size by repacking it; see [./docs/devnotes.md#sfx-repack](./docs/devnotes.md#sfx-repack)
|
||||
|
||||
@@ -2008,6 +2045,16 @@ meanwhile [copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/d
|
||||
then again, if you are already into downloading shady binaries from the internet, you may also want my [minimal builds](./scripts/pyinstaller#ffmpeg) of [ffmpeg](https://ocv.me/stuff/bin/ffmpeg.exe) and [ffprobe](https://ocv.me/stuff/bin/ffprobe.exe) which enables copyparty to extract multimedia-info, do audio-transcoding, and thumbnails/spectrograms/waveforms, however it's much better to instead grab a [recent official build](https://www.gyan.dev/ffmpeg/builds/ffmpeg-git-full.7z) every once ina while if you can afford the size
|
||||
|
||||
|
||||
## zipapp
|
||||
|
||||
another emergency alternative, [copyparty.pyz](https://github.com/9001/copyparty/releases/latest/download/copyparty.pyz) has less features, requires python 3.7 or newer, worse compression, and more importantly is unable to benefit from more recent versions of jinja2 and such (which makes it less secure)... lots of drawbacks with this one really -- but it *may* just work if the regular sfx fails to start because the computer is messed up in certain funky ways, so it's worth a shot if all else fails
|
||||
|
||||
run it by doubleclicking it, or try typing `python copyparty.pyz` in your terminal/console/commandline/telex if that fails
|
||||
|
||||
it is a python [zipapp](https://docs.python.org/3/library/zipapp.html) meaning it doesn't have to unpack its own python code anywhere to run, so if the filesystem is busted it has a better chance of getting somewhere
|
||||
* but note that it currently still needs to extract the web-resources somewhere (they'll land in the default TEMP-folder of your OS)
|
||||
|
||||
|
||||
# install on android
|
||||
|
||||
install [Termux](https://termux.com/) + its companion app `Termux:API` (see [ocv.me/termux](https://ocv.me/termux/)) and then copy-paste this into Termux (long-tap) all at once:
|
||||
|
||||
19
bin/u2c.py
19
bin/u2c.py
@@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
S_VERSION = "1.16"
|
||||
S_BUILD_DT = "2024-04-20"
|
||||
S_VERSION = "1.17"
|
||||
S_BUILD_DT = "2024-05-09"
|
||||
|
||||
"""
|
||||
u2c.py: upload to copyparty
|
||||
@@ -79,12 +79,21 @@ req_ses = requests.Session()
|
||||
|
||||
|
||||
class Daemon(threading.Thread):
|
||||
def __init__(self, target, name=None, a=None):
|
||||
# type: (Any, Any, Any) -> None
|
||||
threading.Thread.__init__(self, target=target, args=a or (), name=name)
|
||||
def __init__(self, target, name = None, a = None):
|
||||
threading.Thread.__init__(self, name=name)
|
||||
self.a = a or ()
|
||||
self.fun = target
|
||||
self.daemon = True
|
||||
self.start()
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
signal.pthread_sigmask(signal.SIG_BLOCK, [signal.SIGINT, signal.SIGTERM])
|
||||
except:
|
||||
pass
|
||||
|
||||
self.fun(*self.a)
|
||||
|
||||
|
||||
class File(object):
|
||||
"""an up2k upload task; represents a single file"""
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Maintainer: icxes <dev.null@need.moe>
|
||||
pkgname=copyparty
|
||||
pkgver="1.12.2"
|
||||
pkgver="1.13.1"
|
||||
pkgrel=1
|
||||
pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++"
|
||||
arch=("any")
|
||||
@@ -21,7 +21,7 @@ optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tag
|
||||
)
|
||||
source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz")
|
||||
backup=("etc/${pkgname}.d/init" )
|
||||
sha256sums=("e4fd6733e5361f5ceb2ae950f71f65f2609c2b69d45f47e8b2a2f128fb67de0a")
|
||||
sha256sums=("f103b784c423a45fbab47c584e4cc53d887fe0616f803bffe009fbfdab3963d7")
|
||||
|
||||
build() {
|
||||
cd "${srcdir}/${pkgname}-${pkgver}"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"url": "https://github.com/9001/copyparty/releases/download/v1.12.2/copyparty-sfx.py",
|
||||
"version": "1.12.2",
|
||||
"hash": "sha256-GJts5N0leK/WHqpqb+eB1JjBvf6TRpzCc9R7AIHkujo="
|
||||
"url": "https://github.com/9001/copyparty/releases/download/v1.13.1/copyparty-sfx.py",
|
||||
"version": "1.13.1",
|
||||
"hash": "sha256-NFfnveCrR1SbiNlibVyU3UPePLUGJMc4XZvWdksXNd8="
|
||||
}
|
||||
60
copyparty/__main__.py
Executable file → Normal file
60
copyparty/__main__.py
Executable file → Normal file
@@ -43,11 +43,13 @@ from .util import (
|
||||
DEF_MTH,
|
||||
IMPLICATIONS,
|
||||
JINJA_VER,
|
||||
MIMES,
|
||||
PARTFTPY_VER,
|
||||
PY_DESC,
|
||||
PYFTPD_VER,
|
||||
SQLITE_VER,
|
||||
UNPLICATIONS,
|
||||
Daemon,
|
||||
align_tab,
|
||||
ansi_re,
|
||||
dedent,
|
||||
@@ -470,6 +472,16 @@ def disable_quickedit() -> None:
|
||||
cmode(True, mode | 4)
|
||||
|
||||
|
||||
def sfx_tpoke(top: str):
|
||||
files = [os.path.join(dp, p) for dp, dd, df in os.walk(top) for p in dd + df]
|
||||
while True:
|
||||
t = int(time.time())
|
||||
for f in [top] + files:
|
||||
os.utime(f, (t, t))
|
||||
|
||||
time.sleep(78123)
|
||||
|
||||
|
||||
def showlic() -> None:
|
||||
p = os.path.join(E.mod, "res", "COPYING.txt")
|
||||
if not os.path.exists(p):
|
||||
@@ -820,7 +832,7 @@ def build_flags_desc():
|
||||
v = v.replace("\n", "\n ")
|
||||
ret += "\n \033[36m{}\033[35m {}".format(k, v)
|
||||
|
||||
return ret + "\033[0m"
|
||||
return ret
|
||||
|
||||
|
||||
# fmt: off
|
||||
@@ -838,6 +850,8 @@ def add_general(ap, nc, srvname):
|
||||
ap2.add_argument("--urlform", metavar="MODE", type=u, default="print,get", help="how to handle url-form POSTs; see \033[33m--help-urlform\033[0m")
|
||||
ap2.add_argument("--wintitle", metavar="TXT", type=u, default="cpp @ $pub", help="server terminal title, for example [\033[32m$ip-10.1.2.\033[0m] or [\033[32m$ip-]")
|
||||
ap2.add_argument("--name", metavar="TXT", type=u, default=srvname, help="server name (displayed topleft in browser and in mDNS)")
|
||||
ap2.add_argument("--mime", metavar="EXT=MIME", type=u, action="append", help="map file \033[33mEXT\033[0mension to \033[33mMIME\033[0mtype, for example [\033[32mjpg=image/jpeg\033[0m]")
|
||||
ap2.add_argument("--mimes", action="store_true", help="list default mimetype mapping and exit")
|
||||
ap2.add_argument("--license", action="store_true", help="show licenses and exit")
|
||||
ap2.add_argument("--version", action="store_true", help="show versions and exit")
|
||||
|
||||
@@ -860,6 +874,7 @@ def add_fs(ap):
|
||||
ap2.add_argument("--rm-retry", metavar="T/R", type=u, default=rm_re_def, help="if a file cannot be deleted because it is busy, continue trying for \033[33mT\033[0m seconds, retry every \033[33mR\033[0m seconds; disable with 0/0 (volflag=rm_retry)")
|
||||
ap2.add_argument("--mv-retry", metavar="T/R", type=u, default=rm_re_def, help="if a file cannot be renamed because it is busy, continue trying for \033[33mT\033[0m seconds, retry every \033[33mR\033[0m seconds; disable with 0/0 (volflag=mv_retry)")
|
||||
ap2.add_argument("--iobuf", metavar="BYTES", type=int, default=256*1024, help="file I/O buffer-size; if your volumes are on a network drive, try increasing to \033[32m524288\033[0m or even \033[32m4194304\033[0m (and let me know if that improves your performance)")
|
||||
ap2.add_argument("--mtab-age", metavar="SEC", type=int, default=60, help="rebuild mountpoint cache every \033[33mSEC\033[0m to keep track of sparse-files support; keep low on servers with removable media")
|
||||
|
||||
|
||||
def add_upload(ap):
|
||||
@@ -1193,7 +1208,8 @@ def add_thumbnail(ap):
|
||||
ap2.add_argument("--th-r-vips", metavar="T,T", type=u, default="avif,exr,fit,fits,fts,gif,hdr,heic,jp2,jpeg,jpg,jpx,jxl,nii,pfm,pgm,png,ppm,svg,tif,tiff,webp", help="image formats to decode using pyvips")
|
||||
ap2.add_argument("--th-r-ffi", metavar="T,T", type=u, default="apng,avif,avifs,bmp,dds,dib,fit,fits,fts,gif,hdr,heic,heics,heif,heifs,icns,ico,jp2,jpeg,jpg,jpx,jxl,pbm,pcx,pfm,pgm,png,pnm,ppm,psd,qoi,sgi,tga,tif,tiff,webp,xbm,xpm", help="image formats to decode using ffmpeg")
|
||||
ap2.add_argument("--th-r-ffv", metavar="T,T", type=u, default="3gp,asf,av1,avc,avi,flv,h264,h265,hevc,m4v,mjpeg,mjpg,mkv,mov,mp4,mpeg,mpeg2,mpegts,mpg,mpg2,mts,nut,ogm,ogv,rm,ts,vob,webm,wmv", help="video formats to decode using ffmpeg")
|
||||
ap2.add_argument("--th-r-ffa", metavar="T,T", type=u, default="aac,ac3,aif,aiff,alac,alaw,amr,apac,ape,au,bonk,dfpwm,dts,flac,gsm,ilbc,it,m4a,mo3,mod,mp2,mp3,mpc,mptm,mt2,mulaw,ogg,okt,opus,ra,s3m,tak,tta,ulaw,wav,wma,wv,xm,xpk", help="audio formats to decode using ffmpeg")
|
||||
ap2.add_argument("--th-r-ffa", metavar="T,T", type=u, default="aac,ac3,aif,aiff,alac,alaw,amr,apac,ape,au,bonk,dfpwm,dts,flac,gsm,ilbc,it,itgz,itxz,itz,m4a,mdgz,mdxz,mdz,mo3,mod,mp2,mp3,mpc,mptm,mt2,mulaw,ogg,okt,opus,ra,s3m,s3gz,s3xz,s3z,tak,tta,ulaw,wav,wma,wv,xm,xmgz,xmxz,xmz,xpk", help="audio formats to decode using ffmpeg")
|
||||
ap2.add_argument("--au-unpk", metavar="E=F.C", type=u, default="mdz=mod.zip, mdgz=mod.gz, mdxz=mod.xz, s3z=s3m.zip, s3gz=s3m.gz, s3xz=s3m.xz, xmz=xm.zip, xmgz=xm.gz, xmxz=xm.xz, itz=it.zip, itgz=it.gz, itxz=it.xz", help="audio formats to decompress before passing to ffmpeg")
|
||||
|
||||
|
||||
def add_transcoding(ap):
|
||||
@@ -1256,19 +1272,38 @@ def add_txt(ap):
|
||||
ap2.add_argument("--exp-lg", metavar="V,V,V", type=u, default=DEF_EXP, help="comma/space-separated list of placeholders to expand in prologue/epilogue files (volflag=exp_lg)")
|
||||
|
||||
|
||||
def add_og(ap):
|
||||
ap2 = ap.add_argument_group('og / open graph / discord-embed options')
|
||||
ap2.add_argument("--og", action="store_true", help="disable hotlinking and return an html document instead; this is required by open-graph, but can also be useful on its own (volflag=og)")
|
||||
ap2.add_argument("--og-ua", metavar="RE", type=u, default="", help="only disable hotlinking / engage OG behavior if the useragent matches regex \033[33mRE\033[0m (volflag=og_ua)")
|
||||
ap2.add_argument("--og-tpl", metavar="PATH", type=u, default="", help="do not return the regular copyparty html, but instead load the jinja2 template at \033[33mPATH\033[0m (if path contains 'EXT' then EXT will be replaced with the requested file's extension) (volflag=og_tpl)")
|
||||
ap2.add_argument("--og-no-head", action="store_true", help="do not automatically add OG entries into <head> (useful if you're doing this yourself in a template or such) (volflag=og_no_head)")
|
||||
ap2.add_argument("--og-th", metavar="FMT", type=u, default="jf3", help="thumbnail format; j=jpeg, jf=jpeg-uncropped, jf3=jpeg-uncropped-large, w=webm, ... (volflag=og_th)")
|
||||
ap2.add_argument("--og-title", metavar="TXT", type=u, default="", help="fallback title if there is nothing in the \033[33m-e2t\033[0m database (volflag=og_title)")
|
||||
ap2.add_argument("--og-title-a", metavar="T", type=u, default="🎵 {{ artist }} - {{ title }}", help="audio title format; takes any metadata key (volflag=og_title_a)")
|
||||
ap2.add_argument("--og-title-v", metavar="T", type=u, default="{{ title }}", help="video title format; takes any metadata key (volflag=og_title_v)")
|
||||
ap2.add_argument("--og-title-i", metavar="T", type=u, default="{{ title }}", help="image title format; takes any metadata key (volflag=og_title_i)")
|
||||
ap2.add_argument("--og-s-title", action="store_true", help="force default title; do not read from tags (volflag=og_s_title)")
|
||||
ap2.add_argument("--og-desc", metavar="TXT", type=u, default="", help="description text; same for all files, disable with [\033[32m-\033[0m] (volflag=og_desc)")
|
||||
ap2.add_argument("--og-site", metavar="TXT", type=u, default="", help="sitename; defaults to \033[33m--name\033[0m, disable with [\033[32m-\033[0m] (volflag=og_site)")
|
||||
ap2.add_argument("--tcolor", metavar="RGB", type=u, default="333", help="accent color (3 or 6 hex digits); may also affect safari and/or android-chrome (volflag=tcolor)")
|
||||
ap2.add_argument("--uqe", action="store_true", help="query-string parceling; translate a request for \033[33m/foo/.uqe/BASE64\033[0m into \033[33m/foo?TEXT\033[0m, or \033[33m/foo/?TEXT\033[0m if the first character in \033[33mTEXT\033[0m is a slash. Automatically enabled for \033[33m--og\033[0m")
|
||||
|
||||
|
||||
def add_ui(ap, retry):
|
||||
ap2 = ap.add_argument_group('ui options')
|
||||
ap2.add_argument("--grid", action="store_true", help="show grid/thumbnails by default (volflag=grid)")
|
||||
ap2.add_argument("--lang", metavar="LANG", type=u, default="eng", help="language; one of the following: \033[32meng nor\033[0m")
|
||||
ap2.add_argument("--theme", metavar="NUM", type=int, default=0, help="default theme to use (0..7)")
|
||||
ap2.add_argument("--themes", metavar="NUM", type=int, default=8, help="number of themes installed")
|
||||
ap2.add_argument("--au-vol", metavar="0-100", type=int, default=50, choices=range(0, 101), help="default audio/video volume percent")
|
||||
ap2.add_argument("--sort", metavar="C,C,C", type=u, default="href", help="default sort order, comma-separated column IDs (see header tooltips), prefix with '-' for descending. Examples: \033[32mhref -href ext sz ts tags/Album tags/.tn\033[0m (volflag=sort)")
|
||||
ap2.add_argument("--unlist", metavar="REGEX", type=u, default="", help="don't show files matching \033[33mREGEX\033[0m in file list. Purely cosmetic! Does not affect API calls, just the browser. Example: [\033[32m\\.(js|css)$\033[0m] (volflag=unlist)")
|
||||
ap2.add_argument("--favico", metavar="TXT", type=u, default="c 000 none" if retry else "🎉 000 none", help="\033[33mfavicon-text\033[0m [ \033[33mforeground\033[0m [ \033[33mbackground\033[0m ] ], set blank to disable")
|
||||
ap2.add_argument("--mpmc", metavar="URL", type=u, default="", help="change the mediaplayer-toggle mouse cursor; URL to a folder with {2..5}.png inside (or disable with [\033[32m.\033[0m])")
|
||||
ap2.add_argument("--js-browser", metavar="L", type=u, help="URL to additional JS to include")
|
||||
ap2.add_argument("--css-browser", metavar="L", type=u, help="URL to additional CSS to include")
|
||||
ap2.add_argument("--html-head", metavar="TXT", type=u, default="", help="text to append to the <head> of all HTML pages")
|
||||
ap2.add_argument("--html-head", metavar="TXT", type=u, default="", help="text to append to the <head> of all HTML pages; can be @PATH to send the contents of a file at PATH, and/or begin with %% to render as jinja2 template (volflag=html_head)")
|
||||
ap2.add_argument("--ih", action="store_true", help="if a folder contains index.html, show that instead of the directory listing by default (can be changed in the client settings UI, or add ?v to URL for override)")
|
||||
ap2.add_argument("--textfiles", metavar="CSV", type=u, default="txt,nfo,diz,cue,readme", help="file extensions to present as plaintext")
|
||||
ap2.add_argument("--txt-max", metavar="KiB", type=int, default=64, help="max size of embedded textfiles on ?doc= (anything bigger will be lazy-loaded by JS)")
|
||||
@@ -1356,6 +1391,7 @@ def run_argparse(
|
||||
add_hooks(ap)
|
||||
add_stats(ap)
|
||||
add_txt(ap)
|
||||
add_og(ap)
|
||||
add_ui(ap, retry)
|
||||
add_admin(ap)
|
||||
add_logging(ap)
|
||||
@@ -1384,18 +1420,22 @@ def run_argparse(
|
||||
k2 = "help_" + k.replace("-", "_")
|
||||
if vars(ret)[k2]:
|
||||
lprint("# %s help page (%s)" % (k, h))
|
||||
lprint(t + "\033[0m")
|
||||
lprint(t.rstrip() + "\033[0m")
|
||||
sys.exit(0)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
def main(argv: Optional[list[str]] = None) -> None:
|
||||
def main(argv: Optional[list[str]] = None, rsrc: Optional[str] = None) -> None:
|
||||
time.strptime("19970815", "%Y%m%d") # python#7980
|
||||
if WINDOWS:
|
||||
os.system("rem") # enables colors
|
||||
|
||||
init_E(E)
|
||||
|
||||
if rsrc: # pyz
|
||||
E.mod = rsrc
|
||||
|
||||
if argv is None:
|
||||
argv = sys.argv
|
||||
|
||||
@@ -1419,9 +1459,19 @@ def main(argv: Optional[list[str]] = None) -> None:
|
||||
showlic()
|
||||
sys.exit(0)
|
||||
|
||||
if "--mimes" in argv:
|
||||
print("\n".join("%8s %s" % (k, v) for k, v in sorted(MIMES.items())))
|
||||
sys.exit(0)
|
||||
|
||||
if EXE:
|
||||
print("pybin: {}\n".format(pybin), end="")
|
||||
|
||||
for n, zs in enumerate(argv):
|
||||
if zs.startswith("--sfx-tpoke="):
|
||||
Daemon(sfx_tpoke, "sfx-tpoke", (zs.split("=", 1)[1],))
|
||||
argv.pop(n)
|
||||
break
|
||||
|
||||
ensure_locale()
|
||||
|
||||
ensure_webdeps()
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# coding: utf-8
|
||||
|
||||
VERSION = (1, 13, 0)
|
||||
VERSION = (1, 13, 2)
|
||||
CODENAME = "race the beam"
|
||||
BUILD_DT = (2024, 4, 20)
|
||||
BUILD_DT = (2024, 5, 10)
|
||||
|
||||
S_VERSION = ".".join(map(str, VERSION))
|
||||
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)
|
||||
|
||||
@@ -17,7 +17,9 @@ from .bos import bos
|
||||
from .cfg import flagdescs, permdescs, vf_bmap, vf_cmap, vf_vmap
|
||||
from .pwhash import PWHash
|
||||
from .util import (
|
||||
EXTS,
|
||||
IMPLICATIONS,
|
||||
MIMES,
|
||||
SQLITE_VER,
|
||||
UNPLICATIONS,
|
||||
UTC,
|
||||
@@ -1442,6 +1444,7 @@ class AuthSrv(object):
|
||||
elif "" not in mount:
|
||||
# there's volumes but no root; make root inaccessible
|
||||
vfs = VFS(self.log_func, "", "", AXS(), {})
|
||||
vfs.flags["tcolor"] = self.args.tcolor
|
||||
vfs.flags["d2d"] = True
|
||||
|
||||
maxdepth = 0
|
||||
@@ -1727,7 +1730,11 @@ class AuthSrv(object):
|
||||
if self.args.e2d or "e2ds" in vol.flags:
|
||||
vol.flags["e2d"] = True
|
||||
|
||||
for ga, vf in [["no_hash", "nohash"], ["no_idx", "noidx"]]:
|
||||
for ga, vf in [
|
||||
["no_hash", "nohash"],
|
||||
["no_idx", "noidx"],
|
||||
["og_ua", "og_ua"],
|
||||
]:
|
||||
if vf in vol.flags:
|
||||
ptn = re.compile(vol.flags.pop(vf))
|
||||
else:
|
||||
@@ -1773,6 +1780,13 @@ class AuthSrv(object):
|
||||
t = 'volume "/%s" has invalid %stry [%s]'
|
||||
raise Exception(t % (vol.vpath, k, vol.flags.get(k + "try")))
|
||||
|
||||
if vol.flags.get("og"):
|
||||
self.args.uqe = True
|
||||
|
||||
zs = str(vol.flags.get("tcolor", "")).lstrip("#")
|
||||
if len(zs) == 3: # fc5 => ffcc55
|
||||
vol.flags["tcolor"] = "".join([x * 2 for x in zs])
|
||||
|
||||
for k1, k2 in IMPLICATIONS:
|
||||
if k1 in vol.flags:
|
||||
vol.flags[k2] = True
|
||||
@@ -2053,6 +2067,13 @@ class AuthSrv(object):
|
||||
|
||||
self.re_pwd = re.compile(zs)
|
||||
|
||||
# to ensure it propagates into tcpsrv with mp on
|
||||
if self.args.mime:
|
||||
for zs in self.args.mime:
|
||||
ext, mime = zs.split("=", 1)
|
||||
MIMES[ext] = mime
|
||||
EXTS.update({v: k for k, v in MIMES.items()})
|
||||
|
||||
def setup_pwhash(self, acct: dict[str, str]) -> None:
|
||||
self.ah = PWHash(self.args)
|
||||
if not self.ah.on:
|
||||
@@ -2410,7 +2431,7 @@ def expand_config_file(
|
||||
if not cnames:
|
||||
t = "warning: tried to read config-files from folder '%s' but it does not contain any "
|
||||
if names:
|
||||
t += ".conf files; the following files were ignored: %s"
|
||||
t += ".conf files; the following files/subfolders were ignored: %s"
|
||||
t = t % (fp, ", ".join(names[:8]))
|
||||
else:
|
||||
t += "files at all"
|
||||
|
||||
@@ -57,11 +57,8 @@ class BrokerMp(object):
|
||||
def shutdown(self) -> None:
|
||||
self.log("broker", "shutting down")
|
||||
for n, proc in enumerate(self.procs):
|
||||
thr = threading.Thread(
|
||||
target=proc.q_pend.put((0, "shutdown", [])),
|
||||
name="mp-shutdown-{}-{}".format(n, len(self.procs)),
|
||||
)
|
||||
thr.start()
|
||||
name = "mp-shut-%d-%d" % (n, len(self.procs))
|
||||
Daemon(proc.q_pend.put, name, ((0, "shutdown", []),))
|
||||
|
||||
with self.mutex:
|
||||
procs = self.procs
|
||||
|
||||
@@ -39,6 +39,9 @@ def vf_bmap() -> dict[str, str]:
|
||||
"magic",
|
||||
"no_sb_md",
|
||||
"no_sb_lg",
|
||||
"og",
|
||||
"og_no_head",
|
||||
"og_s_title",
|
||||
"rand",
|
||||
"xdev",
|
||||
"xlink",
|
||||
@@ -61,12 +64,23 @@ def vf_vmap() -> dict[str, str]:
|
||||
}
|
||||
for k in (
|
||||
"dbd",
|
||||
"html_head",
|
||||
"lg_sbf",
|
||||
"md_sbf",
|
||||
"nrand",
|
||||
"og_desc",
|
||||
"og_site",
|
||||
"og_th",
|
||||
"og_title",
|
||||
"og_title_a",
|
||||
"og_title_v",
|
||||
"og_title_i",
|
||||
"og_tpl",
|
||||
"og_ua",
|
||||
"mv_retry",
|
||||
"rm_retry",
|
||||
"sort",
|
||||
"tcolor",
|
||||
"unlist",
|
||||
"u2abort",
|
||||
"u2ts",
|
||||
@@ -81,7 +95,6 @@ def vf_cmap() -> dict[str, str]:
|
||||
for k in (
|
||||
"exp_lg",
|
||||
"exp_md",
|
||||
"html_head",
|
||||
"mte",
|
||||
"mth",
|
||||
"mtp",
|
||||
@@ -177,6 +190,7 @@ flagcats = {
|
||||
"dvthumb": "disables video thumbnails",
|
||||
"dathumb": "disables audio thumbnails (spectrograms)",
|
||||
"dithumb": "disables image thumbnails",
|
||||
"pngquant": "compress audio waveforms 33% better",
|
||||
"thsize": "thumbnail res; WxH",
|
||||
"crop": "center-cropping (y/n/fy/fn)",
|
||||
"th3x": "3x resolution (y/n/fy/fn)",
|
||||
@@ -201,7 +215,7 @@ flagcats = {
|
||||
"grid": "show grid/thumbnails by default",
|
||||
"sort": "default sort order",
|
||||
"unlist": "dont list files matching REGEX",
|
||||
"html_head=TXT": "includes TXT in the <head>",
|
||||
"html_head=TXT": "includes TXT in the <head>, or @PATH for file at PATH",
|
||||
"robots": "allows indexing by search engines (default)",
|
||||
"norobots": "kindly asks search engines to leave",
|
||||
"no_sb_md": "disable js sandbox for markdown files",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
@@ -17,20 +18,26 @@ if True: # pylint: disable=using-constant-test
|
||||
|
||||
|
||||
class Fstab(object):
|
||||
def __init__(self, log: "RootLogger"):
|
||||
def __init__(self, log: "RootLogger", args: argparse.Namespace):
|
||||
self.log_func = log
|
||||
|
||||
self.warned = False
|
||||
self.trusted = False
|
||||
self.tab: Optional[VFS] = None
|
||||
self.oldtab: Optional[VFS] = None
|
||||
self.srctab = "a"
|
||||
self.cache: dict[str, str] = {}
|
||||
self.age = 0.0
|
||||
self.maxage = args.mtab_age
|
||||
|
||||
def log(self, msg: str, c: Union[int, str] = 0) -> None:
|
||||
self.log_func("fstab", msg, c)
|
||||
|
||||
def get(self, path: str) -> str:
|
||||
if len(self.cache) > 9000:
|
||||
self.age = time.time()
|
||||
now = time.time()
|
||||
if now - self.age > self.maxage or len(self.cache) > 9000:
|
||||
self.age = now
|
||||
self.oldtab = self.tab or self.oldtab
|
||||
self.tab = None
|
||||
self.cache = {}
|
||||
|
||||
@@ -75,7 +82,7 @@ class Fstab(object):
|
||||
self.trusted = False
|
||||
|
||||
def build_tab(self) -> None:
|
||||
self.log("building tab")
|
||||
self.log("inspecting mtab for changes")
|
||||
|
||||
sptn = r"^.*? on (.*) type ([^ ]+) \(.*"
|
||||
if MACOS:
|
||||
@@ -84,6 +91,7 @@ class Fstab(object):
|
||||
ptn = re.compile(sptn)
|
||||
so, _ = chkcmd(["mount"])
|
||||
tab1: list[tuple[str, str]] = []
|
||||
atab = []
|
||||
for ln in so.split("\n"):
|
||||
m = ptn.match(ln)
|
||||
if not m:
|
||||
@@ -91,6 +99,15 @@ class Fstab(object):
|
||||
|
||||
zs1, zs2 = m.groups()
|
||||
tab1.append((str(zs1), str(zs2)))
|
||||
atab.append(ln)
|
||||
|
||||
# keep empirically-correct values if mounttab unchanged
|
||||
srctab = "\n".join(sorted(atab))
|
||||
if srctab == self.srctab:
|
||||
self.tab = self.oldtab
|
||||
return
|
||||
|
||||
self.log("mtab has changed; reevaluating support for sparse files")
|
||||
|
||||
tab1.sort(key=lambda x: (len(x[0]), x[0]))
|
||||
path1, fs1 = tab1[0]
|
||||
@@ -99,6 +116,7 @@ class Fstab(object):
|
||||
tab.add(fs, path.lstrip("/"))
|
||||
|
||||
self.tab = tab
|
||||
self.srctab = srctab
|
||||
|
||||
def relabel(self, path: str, nval: str) -> None:
|
||||
assert self.tab
|
||||
@@ -133,7 +151,9 @@ class Fstab(object):
|
||||
self.trusted = True
|
||||
except:
|
||||
# prisonparty or other restrictive environment
|
||||
self.log("failed to build tab:\n{}".format(min_ex()), 3)
|
||||
if not self.warned:
|
||||
self.warned = True
|
||||
self.log("failed to build tab:\n{}".format(min_ex()), 3)
|
||||
self.build_fallback()
|
||||
|
||||
assert self.tab
|
||||
|
||||
@@ -84,6 +84,9 @@ from .util import (
|
||||
sanitize_vpath,
|
||||
sendfile_kern,
|
||||
sendfile_py,
|
||||
ub64dec,
|
||||
ub64enc,
|
||||
ujoin,
|
||||
undot,
|
||||
unescape_cookie,
|
||||
unquotep,
|
||||
@@ -217,6 +220,13 @@ class HttpCli(object):
|
||||
ka["favico"] = self.args.favico
|
||||
ka["s_name"] = self.args.bname
|
||||
ka["s_doctitle"] = self.args.doctitle
|
||||
ka["tcolor"] = self.vn.flags["tcolor"]
|
||||
|
||||
zso = self.vn.flags.get("html_head")
|
||||
if zso:
|
||||
ka["this"] = self
|
||||
self._build_html_head(zso, ka)
|
||||
|
||||
ka["html_head"] = self.html_head
|
||||
return tpl.render(**ka) # type: ignore
|
||||
|
||||
@@ -364,6 +374,21 @@ class HttpCli(object):
|
||||
if "&" in self.req and "?" not in self.req:
|
||||
self.hint = "did you mean '?' instead of '&'"
|
||||
|
||||
if self.args.uqe and "/.uqe/" in self.req:
|
||||
try:
|
||||
vpath, query = self.req.split("?")[0].split("/.uqe/")
|
||||
query = query.split("/")[0] # discard trailing junk
|
||||
# (usually a "filename" to trick discord into behaving)
|
||||
query = ub64dec(query.encode("utf-8")).decode("utf-8", "replace")
|
||||
if query.startswith("/"):
|
||||
self.req = "%s/?%s" % (vpath, query[1:])
|
||||
else:
|
||||
self.req = "%s?%s" % (vpath, query)
|
||||
except Exception as ex:
|
||||
t = "bad uqe in request [%s]: %r" % (self.req, ex)
|
||||
self.loud_reply(t, status=400)
|
||||
return False
|
||||
|
||||
# split req into vpath + uparam
|
||||
uparam = {}
|
||||
if "?" not in self.req:
|
||||
@@ -430,7 +455,8 @@ class HttpCli(object):
|
||||
cookie_pw = ""
|
||||
|
||||
if len(uparam) > 10 or len(cookies) > 50:
|
||||
raise Pebkac(400, "u wot m8")
|
||||
self.loud_reply("u wot m8", status=400)
|
||||
return False
|
||||
|
||||
self.uparam = uparam
|
||||
self.cookies = cookies
|
||||
@@ -718,6 +744,31 @@ class HttpCli(object):
|
||||
or ("; Trident/" in self.ua and not k304)
|
||||
)
|
||||
|
||||
def _build_html_head(self, maybe_html: Any, kv: dict[str, Any]) -> bool:
|
||||
html = str(maybe_html)
|
||||
is_jinja = html[:2] in "%@%"
|
||||
if is_jinja:
|
||||
html = html.replace("%", "", 1)
|
||||
|
||||
if html.startswith("@"):
|
||||
with open(html[1:], "rb") as f:
|
||||
html = f.read().decode("utf-8")
|
||||
|
||||
if html.startswith("%"):
|
||||
html = html[1:]
|
||||
is_jinja = True
|
||||
|
||||
if is_jinja:
|
||||
print("applying jinja")
|
||||
with self.conn.hsrv.mutex:
|
||||
if html not in self.conn.hsrv.j2:
|
||||
j2env = jinja2.Environment()
|
||||
tpl = j2env.from_string(html)
|
||||
self.conn.hsrv.j2[html] = tpl
|
||||
html = self.conn.hsrv.j2[html].render(**kv)
|
||||
|
||||
self.html_head += html + "\n"
|
||||
|
||||
def send_headers(
|
||||
self,
|
||||
length: Optional[int],
|
||||
@@ -2251,6 +2302,10 @@ class HttpCli(object):
|
||||
def handle_login(self) -> bool:
|
||||
assert self.parser
|
||||
pwd = self.parser.require("cppwd", 64)
|
||||
try:
|
||||
uhash = self.parser.require("uhash", 256)
|
||||
except:
|
||||
uhash = ""
|
||||
self.parser.drop()
|
||||
|
||||
self.out_headerlist = [
|
||||
@@ -2263,6 +2318,11 @@ class HttpCli(object):
|
||||
|
||||
dst += self.ourlq()
|
||||
|
||||
uhash = uhash.lstrip("#")
|
||||
if uhash not in ("", "-"):
|
||||
dst += "&" if "?" in dst else "?"
|
||||
dst += "_=1#" + html_escape(uhash, True, True)
|
||||
|
||||
msg = self.get_pwd_cookie(pwd)
|
||||
html = self.j2s("msg", h1=msg, h2='<a href="' + dst + '">ack</a>', redir=dst)
|
||||
self.reply(html.encode("utf-8"))
|
||||
@@ -3188,7 +3248,7 @@ class HttpCli(object):
|
||||
data_end = file_size
|
||||
break
|
||||
|
||||
if num_need != len(job["need"]):
|
||||
if num_need != len(job["need"]) and data_end - lower < 8 * M:
|
||||
num_need = len(job["need"])
|
||||
data_end = 0
|
||||
for cid in job["hash"]:
|
||||
@@ -3353,7 +3413,7 @@ class HttpCli(object):
|
||||
# for f in fgen: print(repr({k: f[k] for k in ["vp", "ap"]}))
|
||||
cfmt = ""
|
||||
if self.thumbcli and not self.args.no_bacode:
|
||||
for zs in ("opus", "mp3", "w", "j"):
|
||||
for zs in ("opus", "mp3", "w", "j", "p"):
|
||||
if zs in self.ouparam or uarg == zs:
|
||||
cfmt = zs
|
||||
|
||||
@@ -3484,7 +3544,6 @@ class HttpCli(object):
|
||||
targs = {
|
||||
"r": self.args.SR if self.is_vproxied else "",
|
||||
"ts": self.conn.hsrv.cachebuster(),
|
||||
"html_head": self.html_head,
|
||||
"edit": "edit" in self.uparam,
|
||||
"title": html_escape(self.vpath, crlf=True),
|
||||
"lastmod": int(ts_md * 1000),
|
||||
@@ -3495,6 +3554,13 @@ class HttpCli(object):
|
||||
"md": boundary,
|
||||
"arg_base": arg_base,
|
||||
}
|
||||
|
||||
zfv = self.vn.flags.get("html_head")
|
||||
if zfv:
|
||||
targs["this"] = self
|
||||
self._build_html_head(zfv, targs)
|
||||
|
||||
targs["html_head"] = self.html_head
|
||||
zs = template.render(**targs).encode("utf-8", "replace")
|
||||
html = zs.split(boundary.encode("utf-8"))
|
||||
if len(html) != 2:
|
||||
@@ -3610,8 +3676,6 @@ class HttpCli(object):
|
||||
self.reply(zb, mime="text/plain; charset=utf-8")
|
||||
return True
|
||||
|
||||
self.html_head += self.vn.flags.get("html_head", "")
|
||||
|
||||
html = self.j2s(
|
||||
"splash",
|
||||
this=self,
|
||||
@@ -3656,7 +3720,7 @@ class HttpCli(object):
|
||||
return True
|
||||
|
||||
def set_cfg_reset(self) -> bool:
|
||||
for k in ("k304", "js", "idxh", "cppwd", "cppws"):
|
||||
for k in ("k304", "js", "idxh", "dots", "cppwd", "cppws"):
|
||||
cookie = gencookie(k, "x", self.args.R, False)
|
||||
self.out_headerlist.append(("Set-Cookie", cookie))
|
||||
|
||||
@@ -3867,7 +3931,7 @@ class HttpCli(object):
|
||||
allvols = [x for x in allvols if "e2d" in x.flags]
|
||||
|
||||
for vol in allvols:
|
||||
cur = idx.get_cur(vol.realpath)
|
||||
cur = idx.get_cur(vol)
|
||||
if not cur:
|
||||
continue
|
||||
|
||||
@@ -4082,7 +4146,17 @@ class HttpCli(object):
|
||||
e2d = "e2d" in vn.flags
|
||||
e2t = "e2t" in vn.flags
|
||||
|
||||
self.html_head += vn.flags.get("html_head", "")
|
||||
add_og = "og" in vn.flags
|
||||
if add_og:
|
||||
if "th" in self.uparam or "raw" in self.uparam:
|
||||
og_ua = add_og = False
|
||||
elif self.args.og_ua:
|
||||
og_ua = add_og = self.args.og_ua.search(self.ua)
|
||||
else:
|
||||
og_ua = False
|
||||
add_og = True
|
||||
og_fn = ""
|
||||
|
||||
if "b" in self.uparam:
|
||||
self.out_headers["X-Robots-Tag"] = "noindex, nofollow"
|
||||
|
||||
@@ -4090,13 +4164,15 @@ class HttpCli(object):
|
||||
is_dk = False
|
||||
fk_pass = False
|
||||
icur = None
|
||||
if is_dir and (e2t or e2d):
|
||||
if (e2t or e2d) and (is_dir or add_og):
|
||||
idx = self.conn.get_u2idx()
|
||||
if idx and hasattr(idx, "p_end"):
|
||||
icur = idx.get_cur(dbv.realpath)
|
||||
icur = idx.get_cur(dbv)
|
||||
|
||||
th_fmt = self.uparam.get("th")
|
||||
if self.can_read or (self.can_get and vn.flags.get("dk")):
|
||||
if self.can_read or (
|
||||
self.can_get and (vn.flags.get("dk") or "fk" not in vn.flags)
|
||||
):
|
||||
if th_fmt is not None:
|
||||
nothumb = "dthumb" in dbv.flags
|
||||
if is_dir:
|
||||
@@ -4143,7 +4219,7 @@ class HttpCli(object):
|
||||
elif self.can_write and th_fmt is not None:
|
||||
return self.tx_svg("upload\nonly")
|
||||
|
||||
elif self.can_get and self.avn:
|
||||
if not self.can_read and self.can_get and self.avn:
|
||||
axs = self.avn.axs
|
||||
if self.uname not in axs.uhtml:
|
||||
pass
|
||||
@@ -4189,6 +4265,17 @@ class HttpCli(object):
|
||||
self.log(t % (correct, got, self.req, abspath), 6)
|
||||
return self.tx_404()
|
||||
|
||||
if add_og:
|
||||
if og_ua or self.host not in self.headers.get("referer", ""):
|
||||
self.vpath, og_fn = vsplit(self.vpath)
|
||||
vpath = self.vpath
|
||||
vn, rem = self.asrv.vfs.get(self.vpath, self.uname, False, False)
|
||||
abspath = vn.dcanonical(rem)
|
||||
dbv, vrem = vn.get_dbv(rem)
|
||||
is_dir = stat.S_ISDIR(st.st_mode)
|
||||
is_dk = True
|
||||
vpnodes.pop()
|
||||
|
||||
if (
|
||||
(abspath.endswith(".md") or self.can_delete)
|
||||
and "nohtml" not in vn.flags
|
||||
@@ -4200,9 +4287,10 @@ class HttpCli(object):
|
||||
):
|
||||
return self.tx_md(vn, abspath)
|
||||
|
||||
return self.tx_file(
|
||||
abspath, None if st.st_size or "nopipe" in vn.flags else vn.realpath
|
||||
)
|
||||
if not add_og or not og_fn:
|
||||
return self.tx_file(
|
||||
abspath, None if st.st_size or "nopipe" in vn.flags else vn.realpath
|
||||
)
|
||||
|
||||
elif is_dir and not self.can_read:
|
||||
if self._use_dirkey(abspath):
|
||||
@@ -4249,7 +4337,11 @@ class HttpCli(object):
|
||||
is_ls = "ls" in self.uparam
|
||||
is_js = self.args.force_js or self.cookies.get("js") == "y"
|
||||
|
||||
if not is_ls and (self.ua.startswith("curl/") or self.ua.startswith("fetch")):
|
||||
if (
|
||||
not is_ls
|
||||
and not add_og
|
||||
and (self.ua.startswith("curl/") or self.ua.startswith("fetch"))
|
||||
):
|
||||
self.uparam["ls"] = "v"
|
||||
is_ls = True
|
||||
|
||||
@@ -4323,6 +4415,7 @@ class HttpCli(object):
|
||||
"dsort": vf["sort"],
|
||||
"dcrop": vf["crop"],
|
||||
"dth3x": vf["th3x"],
|
||||
"dvol": self.args.au_vol,
|
||||
"themes": self.args.themes,
|
||||
"turbolvl": self.args.turbo,
|
||||
"u2j": self.args.u2j,
|
||||
@@ -4374,7 +4467,7 @@ class HttpCli(object):
|
||||
|
||||
for k in ["zip", "tar"]:
|
||||
v = self.uparam.get(k)
|
||||
if v is not None:
|
||||
if v is not None and (not add_og or not og_fn):
|
||||
return self.tx_zip(k, v, self.vpath, vn, rem, [])
|
||||
|
||||
fsroot, vfs_ls, vfs_virt = vn.ls(
|
||||
@@ -4388,6 +4481,10 @@ class HttpCli(object):
|
||||
ls_names = [x[0] for x in vfs_ls]
|
||||
ls_names.extend(list(vfs_virt.keys()))
|
||||
|
||||
if add_og and og_fn and not self.can_read:
|
||||
ls_names = [og_fn]
|
||||
is_js = True
|
||||
|
||||
# check for old versions of files,
|
||||
# [num-backups, most-recent, hist-path]
|
||||
hist: dict[str, tuple[int, float, str]] = {}
|
||||
@@ -4449,12 +4546,14 @@ class HttpCli(object):
|
||||
margin = "DIR"
|
||||
elif add_dk:
|
||||
zs = absreal(fspath)
|
||||
margin = '<a href="%s?k=%s&zip" rel="nofollow">zip</a>' % (
|
||||
margin = '<a href="%s?k=%s&zip=crc" rel="nofollow">zip</a>' % (
|
||||
quotep(href),
|
||||
self.gen_fk(2, self.args.dk_salt, zs, 0, 0)[:add_dk],
|
||||
)
|
||||
else:
|
||||
margin = '<a href="%s?zip" rel="nofollow">zip</a>' % (quotep(href),)
|
||||
margin = '<a href="%s?zip=crc" rel="nofollow">zip</a>' % (
|
||||
quotep(href),
|
||||
)
|
||||
elif fn in hist:
|
||||
margin = '<a href="%s.hist/%s">#%s</a>' % (
|
||||
base,
|
||||
@@ -4598,6 +4697,9 @@ class HttpCli(object):
|
||||
else:
|
||||
taglist = list(tagset)
|
||||
|
||||
if not files and not dirs and not readme and not logues[0] and not logues[1]:
|
||||
logues[1] = "this folder is empty"
|
||||
|
||||
if is_ls:
|
||||
ls_ret["dirs"] = dirs
|
||||
ls_ret["files"] = files
|
||||
@@ -4649,6 +4751,146 @@ class HttpCli(object):
|
||||
if "mth" in vn.flags:
|
||||
j2a["def_hcols"] = list(vn.flags["mth"])
|
||||
|
||||
if add_og and "raw" not in self.uparam:
|
||||
j2a["this"] = self
|
||||
cgv["og_fn"] = og_fn
|
||||
if og_fn and vn.flags.get("og_tpl"):
|
||||
tpl = vn.flags["og_tpl"]
|
||||
if "EXT" in tpl:
|
||||
zs = og_fn.split(".")[-1].lower()
|
||||
tpl2 = tpl.replace("EXT", zs)
|
||||
if os.path.exists(tpl2):
|
||||
tpl = tpl2
|
||||
with self.conn.hsrv.mutex:
|
||||
if tpl not in self.conn.hsrv.j2:
|
||||
tdir, tname = os.path.split(tpl)
|
||||
j2env = jinja2.Environment()
|
||||
j2env.loader = jinja2.FileSystemLoader(tdir)
|
||||
self.conn.hsrv.j2[tpl] = j2env.get_template(tname)
|
||||
thumb = ""
|
||||
is_pic = is_vid = is_au = False
|
||||
covernames = self.args.th_coversd
|
||||
for fn in ls_names:
|
||||
if fn.lower() in covernames:
|
||||
thumb = fn
|
||||
break
|
||||
if og_fn:
|
||||
ext = og_fn.split(".")[-1].lower()
|
||||
if ext in self.thumbcli.thumbable:
|
||||
is_pic = (
|
||||
ext in self.thumbcli.fmt_pil
|
||||
or ext in self.thumbcli.fmt_vips
|
||||
or ext in self.thumbcli.fmt_ffi
|
||||
)
|
||||
is_vid = ext in self.thumbcli.fmt_ffv
|
||||
is_au = ext in self.thumbcli.fmt_ffa
|
||||
if not thumb or not is_au:
|
||||
thumb = og_fn
|
||||
file = next((x for x in files if x["name"] == og_fn), None)
|
||||
else:
|
||||
file = None
|
||||
|
||||
url_base = "%s://%s/%s" % (
|
||||
"https" if self.is_https else "http",
|
||||
self.host,
|
||||
self.args.RS + quotep(vpath),
|
||||
)
|
||||
j2a["og_is_pic"] = is_pic
|
||||
j2a["og_is_vid"] = is_vid
|
||||
j2a["og_is_au"] = is_au
|
||||
if thumb:
|
||||
fmt = vn.flags.get("og_th", "j")
|
||||
th_base = ujoin(url_base, quotep(thumb))
|
||||
query = "th=%s&cache" % (fmt,)
|
||||
query = ub64enc(query.encode("utf-8")).decode("utf-8")
|
||||
# discord looks at file extension, not content-type...
|
||||
query += "/th.jpg" if "j" in fmt else "/th.webp"
|
||||
j2a["og_thumb"] = "%s/.uqe/%s" % (th_base, query)
|
||||
|
||||
j2a["og_fn"] = og_fn
|
||||
j2a["og_file"] = file
|
||||
if og_fn:
|
||||
og_fn_q = quotep(og_fn)
|
||||
query = ub64enc(b"raw").decode("utf-8")
|
||||
query += "/%s" % (og_fn_q,)
|
||||
j2a["og_url"] = ujoin(url_base, og_fn_q)
|
||||
j2a["og_raw"] = j2a["og_url"] + "/.uqe/" + query
|
||||
else:
|
||||
j2a["og_url"] = j2a["og_raw"] = url_base
|
||||
|
||||
if not vn.flags.get("og_no_head"):
|
||||
ogh = {"twitter:card": "summary"}
|
||||
|
||||
title = str(vn.flags.get("og_title") or "")
|
||||
|
||||
if thumb:
|
||||
ogh["og:image"] = j2a["og_thumb"]
|
||||
|
||||
zso = vn.flags.get("og_desc") or ""
|
||||
if zso != "-":
|
||||
ogh["og:description"] = str(zso)
|
||||
|
||||
zs = vn.flags.get("og_site") or self.args.name
|
||||
if zs not in ("", "-"):
|
||||
ogh["og:site_name"] = zs
|
||||
|
||||
tagmap = {}
|
||||
if is_au:
|
||||
title = str(vn.flags.get("og_title_a") or "")
|
||||
ogh["og:type"] = "music.song"
|
||||
ogh["og:audio"] = j2a["og_raw"]
|
||||
tagmap = {
|
||||
"artist": "og:music:musician",
|
||||
"album": "og:music:album",
|
||||
".dur": "og:music:duration",
|
||||
}
|
||||
elif is_vid:
|
||||
title = str(vn.flags.get("og_title_v") or "")
|
||||
ogh["og:type"] = "video.other"
|
||||
ogh["og:video"] = j2a["og_raw"]
|
||||
tagmap = {
|
||||
"title": "og:title",
|
||||
".dur": "og:video:duration",
|
||||
}
|
||||
elif is_pic:
|
||||
title = str(vn.flags.get("og_title_i") or "")
|
||||
ogh["twitter:card"] = "summary_large_image"
|
||||
ogh["twitter:image"] = ogh["og:image"] = j2a["og_raw"]
|
||||
|
||||
try:
|
||||
for k, v in file["tags"].items():
|
||||
zs = "{{ %s }}" % (k,)
|
||||
title = title.replace(zs, str(v))
|
||||
except:
|
||||
pass
|
||||
title = re.sub(r"\{\{ [^}]+ \}\}", "", title)
|
||||
while title.startswith(" - "):
|
||||
title = title[3:]
|
||||
while title.endswith(" - "):
|
||||
title = title[:3]
|
||||
|
||||
if vn.flags.get("og_s_title") or not title:
|
||||
title = str(vn.flags.get("og_title") or "")
|
||||
|
||||
for tag, hname in tagmap.items():
|
||||
try:
|
||||
v = file["tags"][tag]
|
||||
if not v:
|
||||
continue
|
||||
ogh[hname] = int(v) if tag == ".dur" else v
|
||||
except:
|
||||
pass
|
||||
|
||||
ogh["og:title"] = title
|
||||
|
||||
oghs = [
|
||||
'\t<meta property="%s" content="%s">'
|
||||
% (k, html_escape(str(v), True, True))
|
||||
for k, v in ogh.items()
|
||||
]
|
||||
zs = self.html_head + "\n%s\n" % ("\n".join(oghs),)
|
||||
self.html_head = zs.replace("\n\n", "\n")
|
||||
|
||||
html = self.j2s(tpl, **j2a)
|
||||
self.reply(html.encode("utf-8", "replace"))
|
||||
return True
|
||||
|
||||
@@ -266,10 +266,7 @@ class HttpSrv(object):
|
||||
msg = "subscribed @ {}:{} f{} p{}".format(hip, port, fno, os.getpid())
|
||||
self.log(self.name, msg)
|
||||
|
||||
def fun() -> None:
|
||||
self.broker.say("cb_httpsrv_up")
|
||||
|
||||
threading.Thread(target=fun, name="sig-hsrv-up1").start()
|
||||
Daemon(self.broker.say, "sig-hsrv-up1", ("cb_httpsrv_up",))
|
||||
|
||||
while not self.stopping:
|
||||
if self.args.log_conn:
|
||||
|
||||
@@ -179,7 +179,7 @@ class Metrics(object):
|
||||
tnbytes = 0
|
||||
tnfiles = 0
|
||||
for vpath, vol in allvols:
|
||||
cur = idx.get_cur(vol.realpath)
|
||||
cur = idx.get_cur(vol)
|
||||
if not cur:
|
||||
continue
|
||||
|
||||
|
||||
@@ -7,12 +7,15 @@ import os
|
||||
import shutil
|
||||
import subprocess as sp
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
from .__init__ import ANYWIN, EXE, PY2, WINDOWS, E, unicode
|
||||
from .authsrv import VFS
|
||||
from .bos import bos
|
||||
from .util import (
|
||||
FFMPEG_URL,
|
||||
REKOBO_LKEY,
|
||||
VF_CAREFUL,
|
||||
fsenc,
|
||||
min_ex,
|
||||
pybin,
|
||||
@@ -20,12 +23,13 @@ from .util import (
|
||||
runcmd,
|
||||
sfsenc,
|
||||
uncyg,
|
||||
wunlink,
|
||||
)
|
||||
|
||||
if True: # pylint: disable=using-constant-test
|
||||
from typing import Any, Union
|
||||
from typing import Any, Optional, Union
|
||||
|
||||
from .util import RootLogger
|
||||
from .util import NamedLogger, RootLogger
|
||||
|
||||
|
||||
def have_ff(scmd: str) -> bool:
|
||||
@@ -107,6 +111,51 @@ class MParser(object):
|
||||
raise Exception()
|
||||
|
||||
|
||||
def au_unpk(log: "NamedLogger", fmt_map: dict[str, str], abspath: str, vn: Optional[VFS] = None) -> str:
|
||||
ret = ""
|
||||
try:
|
||||
ext = abspath.split(".")[-1].lower()
|
||||
au, pk = fmt_map[ext].split(".")
|
||||
|
||||
fd, ret = tempfile.mkstemp("." + au)
|
||||
|
||||
if pk == "gz":
|
||||
import gzip
|
||||
|
||||
fi = gzip.GzipFile(abspath, mode="rb")
|
||||
|
||||
elif pk == "xz":
|
||||
import lzma
|
||||
|
||||
fi = lzma.open(abspath, "rb")
|
||||
|
||||
elif pk == "zip":
|
||||
import zipfile
|
||||
|
||||
zf = zipfile.ZipFile(abspath, "r")
|
||||
zil = zf.infolist()
|
||||
zil = [x for x in zil if x.filename.lower().split(".")[-1] == au]
|
||||
fi = zf.open(zil[0])
|
||||
|
||||
with os.fdopen(fd, "wb") as fo:
|
||||
while True:
|
||||
buf = fi.read(32768)
|
||||
if not buf:
|
||||
break
|
||||
|
||||
fo.write(buf)
|
||||
|
||||
return ret
|
||||
|
||||
except Exception as ex:
|
||||
if ret:
|
||||
t = "failed to decompress audio file [%s]: %r"
|
||||
log(t % (abspath, ex))
|
||||
wunlink(log, ret, vn.flags if vn else VF_CAREFUL)
|
||||
|
||||
return abspath
|
||||
|
||||
|
||||
def ffprobe(
|
||||
abspath: str, timeout: int = 60
|
||||
) -> tuple[dict[str, tuple[int, Any]], dict[str, list[Any]]]:
|
||||
@@ -281,7 +330,7 @@ class MTag(object):
|
||||
or_ffprobe = " or FFprobe"
|
||||
|
||||
if self.backend == "mutagen":
|
||||
self.get = self.get_mutagen
|
||||
self._get = self.get_mutagen
|
||||
try:
|
||||
from mutagen import version # noqa: F401
|
||||
except:
|
||||
@@ -290,7 +339,7 @@ class MTag(object):
|
||||
|
||||
if self.backend == "ffprobe":
|
||||
self.usable = self.can_ffprobe
|
||||
self.get = self.get_ffprobe
|
||||
self._get = self.get_ffprobe
|
||||
self.prefer_mt = True
|
||||
|
||||
if not HAVE_FFPROBE:
|
||||
@@ -460,6 +509,17 @@ class MTag(object):
|
||||
|
||||
return r1
|
||||
|
||||
def get(self, abspath: str) -> dict[str, Union[str, float]]:
|
||||
ext = abspath.split(".")[-1].lower()
|
||||
if ext not in self.args.au_unpk:
|
||||
return self._get(abspath)
|
||||
|
||||
ap = au_unpk(self.log, self.args.au_unpk, abspath)
|
||||
ret = self._get(ap)
|
||||
if ap != abspath:
|
||||
wunlink(self.log, ap, VF_CAREFUL)
|
||||
return ret
|
||||
|
||||
def get_mutagen(self, abspath: str) -> dict[str, Union[str, float]]:
|
||||
ret: dict[str, tuple[int, Any]] = {}
|
||||
|
||||
@@ -553,10 +613,16 @@ class MTag(object):
|
||||
except:
|
||||
raise # might be expected outside cpython
|
||||
|
||||
ext = abspath.split(".")[-1].lower()
|
||||
if ext in self.args.au_unpk:
|
||||
ap = au_unpk(self.log, self.args.au_unpk, abspath)
|
||||
else:
|
||||
ap = abspath
|
||||
|
||||
ret: dict[str, Any] = {}
|
||||
for tagname, parser in sorted(parsers.items(), key=lambda x: (x[1].pri, x[0])):
|
||||
try:
|
||||
cmd = [parser.bin, abspath]
|
||||
cmd = [parser.bin, ap]
|
||||
if parser.bin.endswith(".py"):
|
||||
cmd = [pybin] + cmd
|
||||
|
||||
@@ -593,4 +659,7 @@ class MTag(object):
|
||||
t = "mtag error: tagname {}, parser {}, file {} => {}"
|
||||
self.log(t.format(tagname, parser.bin, abspath, min_ex()))
|
||||
|
||||
if ap != abspath:
|
||||
wunlink(self.log, ap, VF_CAREFUL)
|
||||
|
||||
return ret
|
||||
|
||||
@@ -127,7 +127,7 @@ class SMB(object):
|
||||
self.log("smb", msg, c)
|
||||
|
||||
def start(self) -> None:
|
||||
Daemon(self.srv.start)
|
||||
Daemon(self.srv.start, "smbd")
|
||||
|
||||
def _auth_cb(self, *a, **ka):
|
||||
debug("auth-result: %s %s", a, ka)
|
||||
|
||||
@@ -240,6 +240,10 @@ class SvcHub(object):
|
||||
if not HAVE_FFMPEG or not HAVE_FFPROBE:
|
||||
decs.pop("ff", None)
|
||||
|
||||
# compressed formats; "s3z=s3m.zip, s3gz=s3m.gz, ..."
|
||||
zlss = [x.strip().lower().split("=", 1) for x in args.au_unpk.split(",")]
|
||||
args.au_unpk = {x[0]: x[1] for x in zlss}
|
||||
|
||||
self.args.th_dec = list(decs.keys())
|
||||
self.thumbsrv = None
|
||||
want_ff = False
|
||||
@@ -280,6 +284,8 @@ class SvcHub(object):
|
||||
if not re.match("^(0|[qv][0-9]|[0-9]{2,3}k)$", args.q_mp3.lower()):
|
||||
t = "invalid mp3 transcoding quality [%s] specified; only supports [0] to disable, a CBR value such as [192k], or a CQ/CRF value such as [v2]"
|
||||
raise Exception(t % (args.q_mp3,))
|
||||
else:
|
||||
args.au_unpk = {}
|
||||
|
||||
args.th_poke = min(args.th_poke, args.th_maxage, args.ac_maxage)
|
||||
|
||||
@@ -293,13 +299,14 @@ class SvcHub(object):
|
||||
from .ftpd import Ftpd
|
||||
|
||||
self.ftpd: Optional[Ftpd] = None
|
||||
Daemon(self.start_ftpd, "start_ftpd")
|
||||
zms += "f" if args.ftp else "F"
|
||||
|
||||
if args.tftp:
|
||||
from .tftpd import Tftpd
|
||||
|
||||
self.tftpd: Optional[Tftpd] = None
|
||||
|
||||
if args.ftp or args.ftps or args.tftp:
|
||||
Daemon(self.start_ftpd, "start_tftpd")
|
||||
|
||||
if args.smb:
|
||||
@@ -388,7 +395,7 @@ class SvcHub(object):
|
||||
self.sigterm()
|
||||
|
||||
def sigterm(self) -> None:
|
||||
os.kill(os.getpid(), signal.SIGTERM)
|
||||
self.signal_handler(signal.SIGTERM, None)
|
||||
|
||||
def cb_httpsrv_up(self) -> None:
|
||||
self.httpsrv_up += 1
|
||||
@@ -526,7 +533,7 @@ class SvcHub(object):
|
||||
al.exp_md = odfusion(exp, al.exp_md.replace(" ", ","))
|
||||
al.exp_lg = odfusion(exp, al.exp_lg.replace(" ", ","))
|
||||
|
||||
for k in ["no_hash", "no_idx"]:
|
||||
for k in ["no_hash", "no_idx", "og_ua"]:
|
||||
ptn = getattr(self.args, k)
|
||||
if ptn:
|
||||
setattr(self.args, k, re.compile(ptn))
|
||||
@@ -557,6 +564,10 @@ class SvcHub(object):
|
||||
except:
|
||||
raise Exception("invalid --mv-retry [%s]" % (self.args.mv_retry,))
|
||||
|
||||
al.tcolor = al.tcolor.lstrip("#")
|
||||
if len(al.tcolor) == 3: # fc5 => ffcc55
|
||||
al.tcolor = "".join([x * 2 for x in al.tcolor])
|
||||
|
||||
return True
|
||||
|
||||
def _ipa2re(self, txt) -> Optional[re.Pattern]:
|
||||
|
||||
@@ -463,6 +463,12 @@ class TcpSrv(object):
|
||||
sys.stderr.flush()
|
||||
|
||||
def _qr(self, t1: dict[str, list[int]], t2: dict[str, list[int]]) -> str:
|
||||
t2c = {zs: zli for zs, zli in t2.items() if zs in ("127.0.0.1", "::1")}
|
||||
t2b = {zs: zli for zs, zli in t2.items() if ":" in zs and zs not in t2c}
|
||||
t2 = {zs: zli for zs, zli in t2.items() if zs not in t2b and zs not in t2c}
|
||||
t2.update(t2b) # first ipv4, then ipv6...
|
||||
t2.update(t2c) # ...and finally localhost
|
||||
|
||||
ip = None
|
||||
ips = list(t1) + list(t2)
|
||||
qri = self.args.qri
|
||||
|
||||
@@ -107,6 +107,11 @@ class ThumbCli(object):
|
||||
|
||||
fmt = sfmt
|
||||
|
||||
elif fmt[:1] == "p" and not is_au:
|
||||
t = "cannot thumbnail [%s]: png only allowed for waveforms"
|
||||
self.log(t % (rem), 6)
|
||||
return None
|
||||
|
||||
histpath = self.asrv.vfs.histtab.get(ptop)
|
||||
if not histpath:
|
||||
self.log("no histpath for [{}]".format(ptop))
|
||||
|
||||
@@ -15,7 +15,7 @@ from queue import Queue
|
||||
from .__init__ import ANYWIN, TYPE_CHECKING
|
||||
from .authsrv import VFS
|
||||
from .bos import bos
|
||||
from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, ffprobe
|
||||
from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, au_unpk, ffprobe
|
||||
from .util import BytesIO # type: ignore
|
||||
from .util import (
|
||||
FFMPEG_URL,
|
||||
@@ -297,6 +297,12 @@ class ThumbSrv(object):
|
||||
ext = abspath.split(".")[-1].lower()
|
||||
png_ok = False
|
||||
funs = []
|
||||
|
||||
if ext in self.args.au_unpk:
|
||||
ap_unpk = au_unpk(self.log, self.args.au_unpk, abspath, vn)
|
||||
else:
|
||||
ap_unpk = abspath
|
||||
|
||||
if not bos.path.exists(tpath):
|
||||
for lib in self.args.th_dec:
|
||||
if lib == "pil" and ext in self.fmt_pil:
|
||||
@@ -316,9 +322,6 @@ class ThumbSrv(object):
|
||||
else:
|
||||
funs.append(self.conv_spec)
|
||||
|
||||
if not png_ok and tpath.endswith(".png"):
|
||||
raise Pebkac(400, "png only allowed for waveforms")
|
||||
|
||||
tdir, tfn = os.path.split(tpath)
|
||||
ttpath = os.path.join(tdir, "w", tfn)
|
||||
try:
|
||||
@@ -328,7 +331,10 @@ class ThumbSrv(object):
|
||||
|
||||
for fun in funs:
|
||||
try:
|
||||
fun(abspath, ttpath, fmt, vn)
|
||||
if not png_ok and tpath.endswith(".png"):
|
||||
raise Exception("png only allowed for waveforms")
|
||||
|
||||
fun(ap_unpk, ttpath, fmt, vn)
|
||||
break
|
||||
except Exception as ex:
|
||||
msg = "{} could not create thumbnail of {}\n{}"
|
||||
@@ -346,6 +352,9 @@ class ThumbSrv(object):
|
||||
except:
|
||||
pass
|
||||
|
||||
if abspath != ap_unpk:
|
||||
wunlink(self.log, ap_unpk, vn.flags)
|
||||
|
||||
try:
|
||||
wrename(self.log, ttpath, tpath, vn.flags)
|
||||
except:
|
||||
@@ -584,6 +593,24 @@ class ThumbSrv(object):
|
||||
cmd += [fsenc(tpath)]
|
||||
self._run_ff(cmd, vn)
|
||||
|
||||
if "pngquant" in vn.flags:
|
||||
wtpath = tpath + ".png"
|
||||
cmd = [
|
||||
b"pngquant",
|
||||
b"--strip",
|
||||
b"--nofs",
|
||||
b"--output", fsenc(wtpath),
|
||||
fsenc(tpath)
|
||||
]
|
||||
ret = runcmd(cmd, timeout=vn.flags["convt"], nice=True, oom=400)[0]
|
||||
if ret:
|
||||
try:
|
||||
wunlink(self.log, wtpath, vn.flags)
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
wrename(self.log, wtpath, tpath, vn.flags)
|
||||
|
||||
def conv_spec(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
|
||||
ret, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
|
||||
if "ac" not in ret:
|
||||
|
||||
@@ -62,6 +62,17 @@ class U2idx(object):
|
||||
def log(self, msg: str, c: Union[int, str] = 0) -> None:
|
||||
self.log_func("u2idx", msg, c)
|
||||
|
||||
def shutdown(self) -> None:
|
||||
for cur in self.cur.values():
|
||||
db = cur.connection
|
||||
try:
|
||||
db.interrupt()
|
||||
except:
|
||||
pass
|
||||
|
||||
cur.close()
|
||||
db.close()
|
||||
|
||||
def fsearch(
|
||||
self, uname: str, vols: list[VFS], body: dict[str, Any]
|
||||
) -> list[dict[str, Any]]:
|
||||
@@ -81,14 +92,18 @@ class U2idx(object):
|
||||
except:
|
||||
raise Pebkac(500, min_ex())
|
||||
|
||||
def get_cur(self, ptop: str) -> Optional["sqlite3.Cursor"]:
|
||||
def get_cur(self, vn: VFS) -> Optional["sqlite3.Cursor"]:
|
||||
if not HAVE_SQLITE3:
|
||||
return None
|
||||
|
||||
cur = self.cur.get(ptop)
|
||||
cur = self.cur.get(vn.realpath)
|
||||
if cur:
|
||||
return cur
|
||||
|
||||
if "e2d" not in vn.flags:
|
||||
return None
|
||||
|
||||
ptop = vn.realpath
|
||||
histpath = self.asrv.vfs.histtab.get(ptop)
|
||||
if not histpath:
|
||||
self.log("no histpath for [{}]".format(ptop))
|
||||
@@ -317,7 +332,7 @@ class U2idx(object):
|
||||
ptop = vol.realpath
|
||||
flags = vol.flags
|
||||
|
||||
cur = self.get_cur(ptop)
|
||||
cur = self.get_cur(vol)
|
||||
if not cur:
|
||||
continue
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import math
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import signal
|
||||
import stat
|
||||
import subprocess as sp
|
||||
import tempfile
|
||||
@@ -29,6 +28,7 @@ from .fsutil import Fstab
|
||||
from .mtag import MParser, MTag
|
||||
from .util import (
|
||||
HAVE_SQLITE3,
|
||||
VF_CAREFUL,
|
||||
SYMTIME,
|
||||
Daemon,
|
||||
MTHash,
|
||||
@@ -91,9 +91,6 @@ CV_EXTS = set(zsg.split(","))
|
||||
HINT_HISTPATH = "you could try moving the database to another location (preferably an SSD or NVME drive) using either the --hist argument (global option for all volumes), or the hist volflag (just for this volume)"
|
||||
|
||||
|
||||
VF_CAREFUL = {"mv_re_t": 5, "rm_re_t": 5, "mv_re_r": 0.1, "rm_re_r": 0.1}
|
||||
|
||||
|
||||
class Dbw(object):
|
||||
def __init__(self, c: "sqlite3.Cursor", n: int, t: float) -> None:
|
||||
self.c = c
|
||||
@@ -186,7 +183,7 @@ class Up2k(object):
|
||||
t = "could not initialize sqlite3, will use in-memory registry only"
|
||||
self.log(t, 3)
|
||||
|
||||
self.fstab = Fstab(self.log_func)
|
||||
self.fstab = Fstab(self.log_func, self.args)
|
||||
self.gen_fk = self._gen_fk if self.args.log_fk else gen_filekey
|
||||
|
||||
if self.args.hash_mt < 2:
|
||||
@@ -461,7 +458,13 @@ class Up2k(object):
|
||||
# important; not deferred by db_act
|
||||
timeout = self._check_lifetimes()
|
||||
|
||||
timeout = min(timeout, now + self._check_xiu())
|
||||
try:
|
||||
timeout = min(timeout, now + self._check_xiu())
|
||||
except Exception as ex:
|
||||
if "closed cursor" in str(ex):
|
||||
self.log("sched_rescan: lost db")
|
||||
return
|
||||
raise
|
||||
|
||||
with self.mutex:
|
||||
for vp, vol in sorted(self.asrv.vfs.all_vols.items()):
|
||||
@@ -1039,8 +1042,11 @@ class Up2k(object):
|
||||
return None
|
||||
|
||||
def _verify_db_cache(self, cur: "sqlite3.Cursor", vpath: str) -> None:
|
||||
# check if volume config changed since last use; drop caches if so
|
||||
zsl = [vpath] + list(sorted(self.asrv.vfs.all_vols.keys()))
|
||||
# check if list of intersecting volumes changed since last use; drop caches if so
|
||||
prefix = (vpath + "/").lstrip("/")
|
||||
zsl = [x for x in self.asrv.vfs.all_vols if x.startswith(prefix)]
|
||||
zsl = [x[len(prefix) :] for x in zsl]
|
||||
zsl.sort()
|
||||
zb = hashlib.sha1("\n".join(zsl).encode("utf-8", "replace")).digest()
|
||||
vcfg = base64.urlsafe_b64encode(zb[:18]).decode("ascii")
|
||||
|
||||
@@ -1650,7 +1656,7 @@ class Up2k(object):
|
||||
|
||||
if e2vp and rewark:
|
||||
self.hub.retcode = 1
|
||||
os.kill(os.getpid(), signal.SIGTERM)
|
||||
Daemon(self.hub.sigterm)
|
||||
raise Exception("{} files have incorrect hashes".format(len(rewark)))
|
||||
|
||||
if not e2vu or not rewark:
|
||||
@@ -4379,6 +4385,18 @@ class Up2k(object):
|
||||
for x in list(self.spools):
|
||||
self._unspool(x)
|
||||
|
||||
for cur in self.cur.values():
|
||||
db = cur.connection
|
||||
try:
|
||||
db.interrupt()
|
||||
except:
|
||||
pass
|
||||
|
||||
cur.close()
|
||||
db.close()
|
||||
|
||||
self.registry = {}
|
||||
|
||||
|
||||
def up2k_chunksize(filesize: int) -> int:
|
||||
chunksize = 1024 * 1024
|
||||
|
||||
@@ -35,6 +35,9 @@ from .__init__ import ANYWIN, EXE, MACOS, PY2, TYPE_CHECKING, VT100, WINDOWS
|
||||
from .__version__ import S_BUILD_DT, S_VERSION
|
||||
from .stolen import surrogateescape
|
||||
|
||||
ub64dec = base64.urlsafe_b64decode
|
||||
ub64enc = base64.urlsafe_b64encode
|
||||
|
||||
try:
|
||||
from datetime import datetime, timezone
|
||||
|
||||
@@ -355,6 +358,9 @@ APPLESAN_TXT = r"/(__MACOS|Icon\r\r)|/\.(_|DS_Store|AppleDouble|LSOverride|Docum
|
||||
APPLESAN_RE = re.compile(APPLESAN_TXT)
|
||||
|
||||
|
||||
VF_CAREFUL = {"mv_re_t": 5, "rm_re_t": 5, "mv_re_r": 0.1, "rm_re_r": 0.1}
|
||||
|
||||
|
||||
pybin = sys.executable or ""
|
||||
if EXE:
|
||||
pybin = ""
|
||||
@@ -460,13 +466,22 @@ class Daemon(threading.Thread):
|
||||
r: bool = True,
|
||||
ka: Optional[dict[Any, Any]] = None,
|
||||
) -> None:
|
||||
threading.Thread.__init__(
|
||||
self, target=target, name=name, args=a or (), kwargs=ka
|
||||
)
|
||||
threading.Thread.__init__(self, name=name)
|
||||
self.a = a or ()
|
||||
self.ka = ka or {}
|
||||
self.fun = target
|
||||
self.daemon = True
|
||||
if r:
|
||||
self.start()
|
||||
|
||||
def run(self):
|
||||
if not ANYWIN and not PY2:
|
||||
signal.pthread_sigmask(
|
||||
signal.SIG_BLOCK, [signal.SIGINT, signal.SIGTERM, signal.SIGUSR1]
|
||||
)
|
||||
|
||||
self.fun(*self.a, **self.ka)
|
||||
|
||||
|
||||
class Netdev(object):
|
||||
def __init__(self, ip: str, idx: int, name: str, desc: str):
|
||||
@@ -861,6 +876,7 @@ class ProgressPrinter(threading.Thread):
|
||||
self.start()
|
||||
|
||||
def run(self) -> None:
|
||||
sigblock()
|
||||
tp = 0
|
||||
msg = None
|
||||
no_stdout = self.args.q
|
||||
@@ -1305,6 +1321,15 @@ def log_thrs(log: Callable[[str, str, int], None], ival: float, name: str) -> No
|
||||
log(name, "\033[0m \033[33m".join(tv), 3)
|
||||
|
||||
|
||||
def sigblock():
|
||||
if ANYWIN or PY2:
|
||||
return
|
||||
|
||||
signal.pthread_sigmask(
|
||||
signal.SIG_BLOCK, [signal.SIGINT, signal.SIGTERM, signal.SIGUSR1]
|
||||
)
|
||||
|
||||
|
||||
def vol_san(vols: list["VFS"], txt: bytes) -> bytes:
|
||||
txt0 = txt
|
||||
for vol in vols:
|
||||
@@ -1326,10 +1351,11 @@ def vol_san(vols: list["VFS"], txt: bytes) -> bytes:
|
||||
|
||||
def min_ex(max_lines: int = 8, reverse: bool = False) -> str:
|
||||
et, ev, tb = sys.exc_info()
|
||||
stb = traceback.extract_tb(tb)
|
||||
stb = traceback.extract_tb(tb) if tb else traceback.extract_stack()[:-1]
|
||||
fmt = "%s @ %d <%s>: %s"
|
||||
ex = [fmt % (fp.split(os.sep)[-1], ln, fun, txt) for fp, ln, fun, txt in stb]
|
||||
ex.append("[%s] %s" % (et.__name__ if et else "(anonymous)", ev))
|
||||
if et or ev or tb:
|
||||
ex.append("[%s] %s" % (et.__name__ if et else "(anonymous)", ev))
|
||||
return "\n".join(ex[-max_lines:][:: -1 if reverse else 1])
|
||||
|
||||
|
||||
@@ -2031,6 +2057,7 @@ def vsplit(vpath: str) -> tuple[str, str]:
|
||||
return vpath.rsplit("/", 1) # type: ignore
|
||||
|
||||
|
||||
# vpath-join
|
||||
def vjoin(rd: str, fn: str) -> str:
|
||||
if rd and fn:
|
||||
return rd + "/" + fn
|
||||
@@ -2038,6 +2065,14 @@ def vjoin(rd: str, fn: str) -> str:
|
||||
return rd or fn
|
||||
|
||||
|
||||
# url-join
|
||||
def ujoin(rd: str, fn: str) -> str:
|
||||
if rd and fn:
|
||||
return rd.rstrip("/") + "/" + fn.lstrip("/")
|
||||
else:
|
||||
return rd or fn
|
||||
|
||||
|
||||
def _w8dec2(txt: bytes) -> str:
|
||||
"""decodes filesystem-bytes to wtf8"""
|
||||
return surrogateescape.decodefilename(txt)
|
||||
@@ -2669,7 +2704,7 @@ def unescape_cookie(orig: str) -> str:
|
||||
|
||||
def guess_mime(url: str, fallback: str = "application/octet-stream") -> str:
|
||||
try:
|
||||
_, ext = url.rsplit(".", 1)
|
||||
ext = url.rsplit(".", 1)[1].lower()
|
||||
except:
|
||||
return fallback
|
||||
|
||||
|
||||
@@ -740,6 +740,7 @@ window.baguetteBox = (function () {
|
||||
});
|
||||
image.setAttribute('src', imageSrc);
|
||||
if (is_vid) {
|
||||
image.volume = clamp(fcfg_get('vol', dvol / 100), 0, 1);
|
||||
image.setAttribute('controls', 'controls');
|
||||
image.onended = vidEnd;
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@
|
||||
--row-alt: #282828;
|
||||
|
||||
--scroll: #eb0;
|
||||
--sel-fg: var(--bg-d1);
|
||||
--sel-bg: var(--fg);
|
||||
|
||||
--a: #fc5;
|
||||
--a-b: #c90;
|
||||
@@ -330,6 +332,8 @@ html.c {
|
||||
}
|
||||
html.cz {
|
||||
--bgg: var(--bg-u2);
|
||||
--sel-bg: var(--bg-u5);
|
||||
--sel-fg: var(--fg);
|
||||
--srv-3: #fff;
|
||||
--u2-tab-b1: var(--bg-d3);
|
||||
}
|
||||
@@ -343,6 +347,8 @@ html.cy {
|
||||
--bg-d3: #f77;
|
||||
--bg-d2: #ff0;
|
||||
|
||||
--sel-bg: #f77;
|
||||
|
||||
--a: #fff;
|
||||
--a-hil: #fff;
|
||||
--a-h-bg: #000;
|
||||
@@ -588,8 +594,8 @@ html.dy {
|
||||
line-height: 1.2em;
|
||||
}
|
||||
::selection {
|
||||
color: var(--bg-d1);
|
||||
background: var(--fg);
|
||||
color: var(--sel-fg);
|
||||
background: var(--sel-bg);
|
||||
text-shadow: none;
|
||||
}
|
||||
html,body,tr,th,td,#files,a {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<title>{{ title }}</title>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=0.8, minimum-scale=0.6">
|
||||
<meta name="theme-color" content="#333">
|
||||
<meta name="theme-color" content="#{{ tcolor }}">
|
||||
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/ui.css?_={{ ts }}">
|
||||
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/browser.css?_={{ ts }}">
|
||||
{{ html_head }}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"use strict";
|
||||
|
||||
var XHR = XMLHttpRequest;
|
||||
var XHR = XMLHttpRequest,
|
||||
img_re = /\.(a?png|avif|bmp|gif|heif|jpe?g|jfif|svg|webp|webm|mkv|mp4)(\?|$)/i;
|
||||
|
||||
var Ls = {
|
||||
"eng": {
|
||||
"tt": "English",
|
||||
@@ -139,7 +141,7 @@ var Ls = {
|
||||
"ut_rand": "randomize filenames",
|
||||
"ut_u2ts": "copy the last-modified timestamp$Nfrom your filesystem to the server",
|
||||
"ut_mt": "continue hashing other files while uploading$N$Nmaybe disable if your CPU or HDD is a bottleneck",
|
||||
"ut_ask": "ask for confirmation before upload starts",
|
||||
"ut_ask": 'ask for confirmation before upload starts">💭',
|
||||
"ut_pot": "improve upload speed on slow devices$Nby making the UI less complex",
|
||||
"ut_srch": "don't actually upload, instead check if the files already $N exist on the server (will scan all folders you can read)",
|
||||
"ut_par": "pause uploads by setting it to 0$N$Nincrease if your connection is slow / high latency$N$Nkeep it 1 on LAN or if the server HDD is a bottleneck",
|
||||
@@ -192,20 +194,21 @@ var Ls = {
|
||||
"cl_hpick": "tap on column headers to hide in the table below",
|
||||
"cl_hcancel": "column hiding aborted",
|
||||
|
||||
"ct_thumb": "in grid-view, toggle icons or thumbnails$NHotkey: T",
|
||||
"ct_csel": "use CTRL and SHIFT for file selection in grid-view",
|
||||
"ct_ihop": "when the image viewer is closed, scroll down to the last viewed file",
|
||||
"ct_dots": "show hidden files (if server permits)",
|
||||
"ct_dir1st": "sort folders before files",
|
||||
"ct_readme": "show README.md in folder listings",
|
||||
"ct_idxh": "show index.html instead of folder listing",
|
||||
"ct_sbars": "show scrollbars",
|
||||
"ct_grid": '田 the grid',
|
||||
"ct_thumb": 'in grid-view, toggle icons or thumbnails$NHotkey: T">🖼️ thumbs',
|
||||
"ct_csel": 'use CTRL and SHIFT for file selection in grid-view">sel',
|
||||
"ct_ihop": 'when the image viewer is closed, scroll down to the last viewed file">g⮯',
|
||||
"ct_dots": 'show hidden files (if server permits)">dotfiles',
|
||||
"ct_dir1st": 'sort folders before files">📁 first',
|
||||
"ct_readme": 'show README.md in folder listings">📜 readme',
|
||||
"ct_idxh": 'show index.html instead of folder listing">htm',
|
||||
"ct_sbars": 'show scrollbars">⟊',
|
||||
|
||||
"cut_umod": "if a file already exists on the server, update the server's last-modified timestamp to match your local file (requires write+delete permissions)",
|
||||
"cut_umod": "if a file already exists on the server, update the server's last-modified timestamp to match your local file (requires write+delete permissions)\">re📅",
|
||||
|
||||
"cut_turbo": "the yolo button, you probably DO NOT want to enable this:$N$Nuse this if you were uploading a huge amount of files and had to restart for some reason, and want to continue the upload ASAP$N$Nthis replaces the hash-check with a simple <em>"does this have the same filesize on the server?"</em> so if the file contents are different it will NOT be uploaded$N$Nyou should turn this off when the upload is done, and then "upload" the same files again to let the client verify them",
|
||||
"cut_turbo": "the yolo button, you probably DO NOT want to enable this:$N$Nuse this if you were uploading a huge amount of files and had to restart for some reason, and want to continue the upload ASAP$N$Nthis replaces the hash-check with a simple <em>"does this have the same filesize on the server?"</em> so if the file contents are different it will NOT be uploaded$N$Nyou should turn this off when the upload is done, and then "upload" the same files again to let the client verify them\">turbo",
|
||||
|
||||
"cut_datechk": "has no effect unless the turbo button is enabled$N$Nreduces the yolo factor by a tiny amount; checks whether the file timestamps on the server matches yours$N$Nshould <em>theoretically</em> catch most unfinished / corrupted uploads, but is not a substitute for doing a verification pass with turbo disabled afterwards",
|
||||
"cut_datechk": "has no effect unless the turbo button is enabled$N$Nreduces the yolo factor by a tiny amount; checks whether the file timestamps on the server matches yours$N$Nshould <em>theoretically</em> catch most unfinished / corrupted uploads, but is not a substitute for doing a verification pass with turbo disabled afterwards\">date-chk",
|
||||
|
||||
"cut_flag": "ensure only one tab is uploading at a time $N -- other tabs must have this enabled too $N -- only affects tabs on the same domain",
|
||||
|
||||
@@ -214,7 +217,7 @@ var Ls = {
|
||||
"cut_nag": "OS notification when upload completes$N(only if the browser or tab is not active)",
|
||||
"cut_sfx": "audible alert when upload completes$N(only if the browser or tab is not active)",
|
||||
|
||||
"cut_mt": "use multithreading to accelerate file hashing$N$Nthis uses web-workers and requires$Nmore RAM (up to 512 MiB extra)$N$N30% faster https, 4.5x faster http,$Nand 5.3x faster on android phones",
|
||||
"cut_mt": "use multithreading to accelerate file hashing$N$Nthis uses web-workers and requires$Nmore RAM (up to 512 MiB extra)$N$N30% faster https, 4.5x faster http,$Nand 5.3x faster on android phones\">mt",
|
||||
|
||||
"cft_text": "favicon text (blank and refresh to disable)",
|
||||
"cft_fg": "foreground color",
|
||||
@@ -283,6 +286,7 @@ var Ls = {
|
||||
|
||||
"im_hnf": "that image no longer exists",
|
||||
|
||||
"f_empty": 'this folder is empty',
|
||||
"f_chide": 'this will hide the column «{0}»\n\nyou can unhide columns in the settings tab',
|
||||
"f_bigtxt": "this file is {0} MiB large -- really view as text?",
|
||||
"fbd_more": '<div id="blazy">showing <code>{0}</code> of <code>{1}</code> files; <a href="#" id="bd_more">show {2}</a> or <a href="#" id="bd_all">show all</a></div>',
|
||||
@@ -337,6 +341,9 @@ var Ls = {
|
||||
"fp_err": "move failed:\n",
|
||||
"fp_confirm": "move these {0} items here?",
|
||||
"fp_etab": 'failed to read clipboard from other browser tab',
|
||||
"fp_name": "uploading a file from your device. Give it a name:",
|
||||
"fp_both_m": '<h6>choose what to paste</h6><code>Enter</code> = Move {0} files from «{1}»\n<code>ESC</code> = Upload {2} files from your device',
|
||||
"fp_both_b": '<a href="#" id="modal-ok">Move</a><a href="#" id="modal-ng">Upload</a>',
|
||||
|
||||
"mk_noname": "type a name into the text field on the left before you do that :p",
|
||||
|
||||
@@ -644,7 +651,7 @@ var Ls = {
|
||||
"ut_rand": "finn opp nye tilfeldige filnavn",
|
||||
"ut_u2ts": "gi filen på serveren samme$Ntidsstempel som lokalt hos deg",
|
||||
"ut_mt": "fortsett å befare køen mens opplastning foregår$N$Nskru denne av dersom du har en$Ntreg prosessor eller harddisk",
|
||||
"ut_ask": "bekreft filutvalg før opplastning starter",
|
||||
"ut_ask": 'bekreft filutvalg før opplastning starter">💭',
|
||||
"ut_pot": "forbedre ytelsen på trege enheter ved å$Nforenkle brukergrensesnittet",
|
||||
"ut_srch": "utfør søk istedenfor å laste opp --$Nleter igjennom alle mappene du har lov til å se",
|
||||
"ut_par": "sett til 0 for å midlertidig stanse opplastning$N$Nhøye verdier (4 eller 8) kan gi bedre ytelse,$Nspesielt på trege internettlinjer$N$Nbør ikke være høyere enn 1 på LAN$Neller hvis serveren sin harddisk er treg",
|
||||
@@ -697,20 +704,21 @@ var Ls = {
|
||||
"cl_hpick": "klikk på overskriften til kolonnene du ønsker å skjule i tabellen nedenfor",
|
||||
"cl_hcancel": "kolonne-skjuling avbrutt",
|
||||
|
||||
"ct_thumb": "vis miniatyrbilder istedenfor ikoner$NSnarvei: T",
|
||||
"ct_csel": "bruk tastene CTRL og SHIFT for markering av filer i ikonvisning",
|
||||
"ct_ihop": "bla ned til sist viste bilde når bildeviseren lukkes",
|
||||
"ct_dots": "vis skjulte filer (gitt at serveren tillater det)",
|
||||
"ct_dir1st": "sorter slik at mapper kommer foran filer",
|
||||
"ct_readme": "vis README.md nedenfor filene",
|
||||
"ct_idxh": "vis index.html istedenfor fil-liste",
|
||||
"ct_sbars": "vis rullgardiner / skrollefelt",
|
||||
"ct_grid": '田 ikoner',
|
||||
"ct_thumb": 'vis miniatyrbilder istedenfor ikoner$NSnarvei: T">🖼️ bilder',
|
||||
"ct_csel": 'bruk tastene CTRL og SHIFT for markering av filer i ikonvisning">merk',
|
||||
"ct_ihop": 'bla ned til sist viste bilde når bildeviseren lukkes">g⮯',
|
||||
"ct_dots": 'vis skjulte filer (gitt at serveren tillater det)">.synlig',
|
||||
"ct_dir1st": 'sorter slik at mapper kommer foran filer">📁 først',
|
||||
"ct_readme": 'vis README.md nedenfor filene">📜 readme',
|
||||
"ct_idxh": 'vis index.html istedenfor fil-liste">htm',
|
||||
"ct_sbars": 'vis rullgardiner / skrollefelt">⟊',
|
||||
|
||||
"cut_umod": "i tilfelle en fil du laster opp allerede finnes på serveren, så skal serverens tidsstempel oppdateres slik at det stemmer overens med din lokale fil (krever rettighetene write+delete)",
|
||||
"cut_umod": 'i tilfelle en fil du laster opp allerede finnes på serveren, så skal serverens tidsstempel oppdateres slik at det stemmer overens med din lokale fil (krever rettighetene write+delete)">re📅',
|
||||
|
||||
"cut_turbo": "forenklet befaring ved opplastning; bør sannsynlig <em>ikke</em> skrus på:$N$Nnyttig dersom du var midt i en svær opplastning som måtte restartes av en eller annen grunn, og du vil komme igang igjen så raskt som overhodet mulig.$N$Nnår denne er skrudd på så forenkles befaringen kraftig; istedenfor å utføre en trygg sjekk på om filene finnes på serveren i god stand, så sjekkes kun om <em>filstørrelsen</em> stemmer. Så dersom en korrupt fil skulle befinne seg på serveren allerede, på samme sted med samme størrelse og navn, så blir det <em>ikke oppdaget</em>.$N$Ndet anbefales å kun benytte denne funksjonen for å komme seg raskt igjennom selve opplastningen, for så å skru den av, og til slutt "laste opp" de samme filene én gang til -- slik at integriteten kan verifiseres",
|
||||
"cut_turbo": "forenklet befaring ved opplastning; bør sannsynlig <em>ikke</em> skrus på:$N$Nnyttig dersom du var midt i en svær opplastning som måtte restartes av en eller annen grunn, og du vil komme igang igjen så raskt som overhodet mulig.$N$Nnår denne er skrudd på så forenkles befaringen kraftig; istedenfor å utføre en trygg sjekk på om filene finnes på serveren i god stand, så sjekkes kun om <em>filstørrelsen</em> stemmer. Så dersom en korrupt fil skulle befinne seg på serveren allerede, på samme sted med samme størrelse og navn, så blir det <em>ikke oppdaget</em>.$N$Ndet anbefales å kun benytte denne funksjonen for å komme seg raskt igjennom selve opplastningen, for så å skru den av, og til slutt "laste opp" de samme filene én gang til -- slik at integriteten kan verifiseres\">turbo",
|
||||
|
||||
"cut_datechk": "har ingen effekt dersom turbo er avslått$N$Ngjør turbo bittelitt tryggere ved å sjekke datostemplingen på filene (i tillegg til filstørrelse)$N$N<em>burde</em> oppdage og gjenoppta de fleste ufullstendige opplastninger, men er <em>ikke</em> en fullverdig erstatning for å deaktivere turbo og gjøre en skikkelig sjekk",
|
||||
"cut_datechk": "har ingen effekt dersom turbo er avslått$N$Ngjør turbo bittelitt tryggere ved å sjekke datostemplingen på filene (i tillegg til filstørrelse)$N$N<em>burde</em> oppdage og gjenoppta de fleste ufullstendige opplastninger, men er <em>ikke</em> en fullverdig erstatning for å deaktivere turbo og gjøre en skikkelig sjekk\">date-chk",
|
||||
|
||||
"cut_flag": "samkjører nettleserfaner slik at bare én $N kan holde på med befaring / opplastning $N -- andre faner må også ha denne skrudd på $N -- fungerer kun innenfor samme domene",
|
||||
|
||||
@@ -719,7 +727,7 @@ var Ls = {
|
||||
"cut_nag": "meldingsvarsel når opplastning er ferdig$N(kun on nettleserfanen ikke er synlig)",
|
||||
"cut_sfx": "lydvarsel når opplastning er ferdig$N(kun on nettleserfanen ikke er synlig)",
|
||||
|
||||
"cut_mt": "raskere befaring ved å bruke hele CPU'en$N$Ndenne funksjonen anvender web-workers$Nog krever mer RAM (opptil 512 MiB ekstra)$N$N30% raskere https, 4.5x raskere http,$Nog 5.3x raskere på android-telefoner",
|
||||
"cut_mt": "raskere befaring ved å bruke hele CPU'en$N$Ndenne funksjonen anvender web-workers$Nog krever mer RAM (opptil 512 MiB ekstra)$N$N30% raskere https, 4.5x raskere http,$Nog 5.3x raskere på android-telefoner\">mt",
|
||||
|
||||
"cft_text": "ikontekst (blank ut og last siden på nytt for å deaktivere)",
|
||||
"cft_fg": "farge",
|
||||
@@ -788,6 +796,7 @@ var Ls = {
|
||||
|
||||
"im_hnf": "bildet finnes ikke lenger",
|
||||
|
||||
"f_empty": 'denne mappen er tom',
|
||||
"f_chide": 'dette vil skjule kolonnen «{0}»\n\nfanen for "andre innstillinger" lar deg vise kolonnen igjen',
|
||||
"f_bigtxt": "denne filen er hele {0} MiB -- vis som tekst?",
|
||||
"fbd_more": '<div id="blazy">viser <code>{0}</code> av <code>{1}</code> filer; <a href="#" id="bd_more">vis {2}</a> eller <a href="#" id="bd_all">vis alle</a></div>',
|
||||
@@ -842,6 +851,9 @@ var Ls = {
|
||||
"fp_err": "flytting feilet:\n",
|
||||
"fp_confirm": "flytt disse {0} filene hit?",
|
||||
"fp_etab": 'kunne ikke lese listen med filer ifra den andre nettleserfanen',
|
||||
"fp_name": "Laster opp én fil fra enheten din. Velg filnavn:",
|
||||
"fp_both_m": '<h6>hva skal limes inn her?</h6><code>Enter</code> = Flytt {0} filer fra «{1}»\n<code>ESC</code> = Last opp {2} filer fra enheten din',
|
||||
"fp_both_b": '<a href="#" id="modal-ok">Flytt</a><a href="#" id="modal-ng">Last opp</a>',
|
||||
|
||||
"mk_noname": "skriv inn et navn i tekstboksen til venstre først :p",
|
||||
|
||||
@@ -1012,8 +1024,13 @@ var Ls = {
|
||||
"lang_set": "passer det å laste siden på nytt?",
|
||||
},
|
||||
};
|
||||
var LANGS = ["eng", "nor"],
|
||||
L = Ls[sread("cpp_lang", LANGS) || lang] || Ls.eng || Ls.nor;
|
||||
|
||||
var LANGS = ["eng", "nor"];
|
||||
|
||||
if (window.langmod)
|
||||
langmod();
|
||||
|
||||
var L = Ls[sread("cpp_lang", LANGS) || lang] || Ls.eng || Ls.nor;
|
||||
|
||||
for (var a = 0; a < LANGS.length; a++) {
|
||||
for (var b = a + 1; b < LANGS.length; b++) {
|
||||
@@ -1200,15 +1217,15 @@ ebi('op_cfg').innerHTML = (
|
||||
' <h3>' + L.cl_opts + '</h3>\n' +
|
||||
' <div>\n' +
|
||||
' <a id="tooltips" class="tgl btn" href="#" tt="◔ ◡ ◔">ℹ️ tooltips</a>\n' +
|
||||
' <a id="griden" class="tgl btn" href="#" tt="' + L.wt_grid + '">田 the grid</a>\n' +
|
||||
' <a id="thumbs" class="tgl btn" href="#" tt="' + L.ct_thumb + '">🖼️ thumbs</a>\n' +
|
||||
' <a id="csel" class="tgl btn" href="#" tt="' + L.ct_csel + '">sel</a>\n' +
|
||||
' <a id="ihop" class="tgl btn" href="#" tt="' + L.ct_ihop + '">g⮯</a>\n' +
|
||||
' <a id="dotfiles" class="tgl btn" href="#" tt="' + L.ct_dots + '">dotfiles</a>\n' +
|
||||
' <a id="dir1st" class="tgl btn" href="#" tt="' + L.ct_dir1st + '">📁 first</a>\n' +
|
||||
' <a id="ireadme" class="tgl btn" href="#" tt="' + L.ct_readme + '">📜 readme</a>\n' +
|
||||
' <a id="idxh" class="tgl btn" href="#" tt="' + L.ct_idxh + '">htm</a>\n' +
|
||||
' <a id="sbars" class="tgl btn" href="#" tt="' + L.ct_sbars + '">⟊</a>\n' +
|
||||
' <a id="griden" class="tgl btn" href="#" tt="' + L.wt_grid + '">' + L.ct_grid + '</a>\n' +
|
||||
' <a id="thumbs" class="tgl btn" href="#" tt="' + L.ct_thumb + '</a>\n' +
|
||||
' <a id="csel" class="tgl btn" href="#" tt="' + L.ct_csel + '</a>\n' +
|
||||
' <a id="ihop" class="tgl btn" href="#" tt="' + L.ct_ihop + '</a>\n' +
|
||||
' <a id="dotfiles" class="tgl btn" href="#" tt="' + L.ct_dots + '</a>\n' +
|
||||
' <a id="dir1st" class="tgl btn" href="#" tt="' + L.ct_dir1st + '</a>\n' +
|
||||
' <a id="ireadme" class="tgl btn" href="#" tt="' + L.ct_readme + '</a>\n' +
|
||||
' <a id="idxh" class="tgl btn" href="#" tt="' + L.ct_idxh + '</a>\n' +
|
||||
' <a id="sbars" class="tgl btn" href="#" tt="' + L.ct_sbars + '</a>\n' +
|
||||
' </div>\n' +
|
||||
'</div>\n' +
|
||||
'<div>\n' +
|
||||
@@ -1227,11 +1244,11 @@ ebi('op_cfg').innerHTML = (
|
||||
'<div>\n' +
|
||||
' <h3>' + L.cl_uopts + '</h3>\n' +
|
||||
' <div>\n' +
|
||||
' <a id="ask_up" class="tgl btn" href="#" tt="' + L.ut_ask + '">💭</a>\n' +
|
||||
' <a id="umod" class="tgl btn" href="#" tt="' + L.cut_umod + '">re📅</a>\n' +
|
||||
' <a id="hashw" class="tgl btn" href="#" tt="' + L.cut_mt + '">mt</a>\n' +
|
||||
' <a id="u2turbo" class="tgl btn ttb" href="#" tt="' + L.cut_turbo + '">turbo</a>\n' +
|
||||
' <a id="u2tdate" class="tgl btn ttb" href="#" tt="' + L.cut_datechk + '">date-chk</a>\n' +
|
||||
' <a id="ask_up" class="tgl btn" href="#" tt="' + L.ut_ask + '</a>\n' +
|
||||
' <a id="umod" class="tgl btn" href="#" tt="' + L.cut_umod + '</a>\n' +
|
||||
' <a id="hashw" class="tgl btn" href="#" tt="' + L.cut_mt + '</a>\n' +
|
||||
' <a id="u2turbo" class="tgl btn ttb" href="#" tt="' + L.cut_turbo + '</a>\n' +
|
||||
' <a id="u2tdate" class="tgl btn ttb" href="#" tt="' + L.cut_datechk + '</a>\n' +
|
||||
' <a id="flag_en" class="tgl btn" href="#" tt="' + L.cut_flag + '">💤</a>\n' +
|
||||
' <a id="u2sort" class="tgl btn" href="#" tt="' + L.cut_az + '">az</a>\n' +
|
||||
' <a id="upnag" class="tgl btn" href="#" tt="' + L.cut_nag + '">🔔</a>\n' +
|
||||
@@ -1413,6 +1430,12 @@ var ACtx = !IPHONE && (window.AudioContext || window.webkitAudioContext),
|
||||
dk, mp;
|
||||
|
||||
|
||||
if (window.og_fn) {
|
||||
hash0 = 1;
|
||||
hist_replace(vsplit(get_evpath())[0]);
|
||||
}
|
||||
|
||||
|
||||
var mpl = (function () {
|
||||
var have_mctl = 'mediaSession' in navigator && window.MediaMetadata;
|
||||
|
||||
@@ -1677,7 +1700,7 @@ catch (ex) { }
|
||||
|
||||
|
||||
var re_au_native = (can_ogg || have_acode) ? /\.(aac|flac|m4a|mp3|ogg|opus|wav)$/i : /\.(aac|flac|m4a|mp3|wav)$/i,
|
||||
re_au_all = /\.(aac|ac3|aif|aiff|alac|alaw|amr|ape|au|dfpwm|dts|flac|gsm|it|m4a|mo3|mod|mp2|mp3|mpc|mptm|mt2|mulaw|ogg|okt|opus|ra|s3m|tak|tta|ulaw|wav|wma|wv|xm|xpk)$/i;
|
||||
re_au_all = /\.(aac|ac3|aif|aiff|alac|alaw|amr|ape|au|dfpwm|dts|flac|gsm|it|itgz|itxz|itz|m4a|mdgz|mdxz|mdz|mo3|mod|mp2|mp3|mpc|mptm|mt2|mulaw|ogg|okt|opus|ra|s3m|s3gz|s3xz|s3z|tak|tta|ulaw|wav|wma|wv|xm|xmgz|xmxz|xmz|xpk)$/i;
|
||||
|
||||
|
||||
// extract songs + add play column
|
||||
@@ -1714,7 +1737,7 @@ function MPlayer() {
|
||||
}
|
||||
}
|
||||
|
||||
r.vol = clamp(fcfg_get('vol', IPHONE ? 1 : 0.5), 0, 1);
|
||||
r.vol = clamp(fcfg_get('vol', IPHONE ? 1 : dvol / 100), 0, 1);
|
||||
|
||||
r.expvol = function (v) {
|
||||
return 0.5 * v + 0.5 * v * v;
|
||||
@@ -3236,7 +3259,7 @@ function autoplay_blocked(seek) {
|
||||
var tid = mp.au.tid,
|
||||
fn = mp.tracks[tid].split(/\//).pop();
|
||||
|
||||
fn = uricom_dec(fn.replace(/\+/g, ' '));
|
||||
fn = uricom_dec(fn.replace(/\+/g, ' ').split('?')[0]);
|
||||
|
||||
modal.confirm('<h6>' + L.mm_hashplay + '</h6>\n«' + esc(fn) + '»', function () {
|
||||
// chrome 91 may permanently taint on a failed play()
|
||||
@@ -3283,6 +3306,21 @@ function scan_hash(v) {
|
||||
function eval_hash() {
|
||||
window.onpopstate = treectl.onpopfun;
|
||||
|
||||
if (hash0 && window.og_fn) {
|
||||
var all = msel.getall(), mi;
|
||||
for (var a = 0; a < all.length; a++)
|
||||
if (og_fn == uricom_dec(vsplit(all[a].vp)[1].split('?')[0])) {
|
||||
mi = all[a];
|
||||
break;
|
||||
}
|
||||
|
||||
if (mi && img_re.exec(og_fn))
|
||||
hash0 = '#g' + mi.id;
|
||||
|
||||
if (ebi('a' + mi.id))
|
||||
hash0 = '#a' + mi.id;
|
||||
}
|
||||
|
||||
var v = hash0;
|
||||
hash0 = null;
|
||||
if (!v)
|
||||
@@ -3596,6 +3634,7 @@ var fileman = (function () {
|
||||
bdel = ebi('fdel'),
|
||||
bcut = ebi('fcut'),
|
||||
bpst = ebi('fpst'),
|
||||
t_paste,
|
||||
r = {};
|
||||
|
||||
r.clip = null;
|
||||
@@ -4073,8 +4112,73 @@ var fileman = (function () {
|
||||
}
|
||||
};
|
||||
|
||||
r.paste = function (e) {
|
||||
ev(e);
|
||||
document.onpaste = function (e) {
|
||||
var xfer = e.clipboardData || window.clipboardData;
|
||||
if (!xfer || !xfer.files || !xfer.files.length)
|
||||
return;
|
||||
|
||||
var files = [];
|
||||
for (var a = 0, aa = xfer.files.length; a < aa; a++)
|
||||
files.push(xfer.files[a]);
|
||||
|
||||
clearTimeout(t_paste);
|
||||
|
||||
if (!r.clip.length)
|
||||
return r.clip_up(files);
|
||||
|
||||
var src = r.clip.length == 1 ? r.clip[0] : vsplit(r.clip[0])[0],
|
||||
msg = L.fp_both_m.format(r.clip.length, src, files.length);
|
||||
|
||||
modal.confirm(msg, r.paste, function () { r.clip_up(files); }, null, L.fp_both_b);
|
||||
};
|
||||
|
||||
r.clip_up = function (files) {
|
||||
goto_up2k();
|
||||
var good = [], nil = [], bad = [];
|
||||
for (var a = 0, aa = files.length; a < aa; a++) {
|
||||
var fobj = files[a], dst = good;
|
||||
try {
|
||||
if (fobj.size < 1)
|
||||
dst = nil;
|
||||
}
|
||||
catch (ex) {
|
||||
dst = bad;
|
||||
}
|
||||
dst.push([fobj, fobj.name]);
|
||||
}
|
||||
var doit = function (is_img) {
|
||||
jwrite('fman_clip', [Date.now()]);
|
||||
r.clip = [];
|
||||
|
||||
var x = up2k.uc.ask_up;
|
||||
if (is_img)
|
||||
up2k.uc.ask_up = false;
|
||||
|
||||
up2k.gotallfiles[0](good, nil, bad, up2k.gotallfiles.slice(1));
|
||||
up2k.uc.ask_up = x;
|
||||
};
|
||||
if (good.length != 1)
|
||||
return doit();
|
||||
|
||||
var fn = good[0][1],
|
||||
ofs = fn.lastIndexOf('.');
|
||||
|
||||
// stop linux-chrome from adding the fs-path into the <input>
|
||||
setTimeout(function () {
|
||||
modal.prompt(L.fp_name, fn, function (v) {
|
||||
good[0][1] = v;
|
||||
doit(true);
|
||||
}, null, null, 0, ofs > 0 ? ofs : undefined);
|
||||
}, 1);
|
||||
};
|
||||
|
||||
r.d_paste = function () {
|
||||
// gets called before onpaste; defer
|
||||
clearTimeout(t_paste);
|
||||
t_paste = setTimeout(r.paste, 50);
|
||||
};
|
||||
|
||||
r.paste = function () {
|
||||
if (clgot(bpst, 'hide'))
|
||||
return toast.err(3, L.fp_eperm);
|
||||
|
||||
@@ -4683,8 +4787,8 @@ var thegrid = (function () {
|
||||
fid = oth.getAttribute('id'),
|
||||
aplay = ebi('a' + fid),
|
||||
atext = ebi('t' + fid),
|
||||
is_txt = atext && showfile.getlang(href),
|
||||
is_img = /\.(a?png|avif|bmp|gif|heif|jpe?g|jfif|svg|webp|webm|mkv|mp4)(\?|$)/i.test(href),
|
||||
is_txt = atext && showfile.getlang(href) && !/\.ts$/.test(href),
|
||||
is_img = img_re.test(href),
|
||||
is_dir = href.endsWith('/'),
|
||||
is_srch = !!ebi('unsearch'),
|
||||
in_tree = is_dir && treectl.find(oth.textContent.slice(0, -1)),
|
||||
@@ -5178,6 +5282,9 @@ document.onkeydown = function (e) {
|
||||
if (ebi('hkhelp'))
|
||||
return qsr('#hkhelp');
|
||||
|
||||
if (toast.visible)
|
||||
return toast.hide();
|
||||
|
||||
if (ebi('rn_cancel'))
|
||||
return ebi('rn_cancel').click();
|
||||
|
||||
@@ -5250,7 +5357,7 @@ document.onkeydown = function (e) {
|
||||
return fileman.cut();
|
||||
|
||||
if (k == 'KeyV' || k == 'v')
|
||||
return fileman.paste();
|
||||
return fileman.d_paste();
|
||||
|
||||
if (k == 'KeyK' || k == 'k')
|
||||
return fileman.delete();
|
||||
@@ -6286,6 +6393,9 @@ var treectl = (function () {
|
||||
lg1 = res.logues ? res.logues[1] || "" : "",
|
||||
dirchg = get_evpath() != cdir;
|
||||
|
||||
if (lg1 === Ls.eng.f_empty)
|
||||
lg1 = L.f_empty;
|
||||
|
||||
sandbox(ebi('pro'), sb_lg, '', lg0);
|
||||
if (dirchg)
|
||||
sandbox(ebi('epi'), sb_lg, '', lg1);
|
||||
@@ -7923,6 +8033,9 @@ window.addEventListener("message", function (e) {
|
||||
|
||||
|
||||
if (sb_lg && logues.length) {
|
||||
if (logues[1] === Ls.eng.f_empty)
|
||||
logues[1] = L.f_empty;
|
||||
|
||||
sandbox(ebi('pro'), sb_lg, '', logues[0]);
|
||||
sandbox(ebi('epi'), sb_lg, '', logues[1]);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<title>📝 {{ title }}</title>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=0.7">
|
||||
<meta name="theme-color" content="#333">
|
||||
<meta name="theme-color" content="#{{ tcolor }}">
|
||||
<link rel="stylesheet" href="{{ r }}/.cpr/ui.css?_={{ ts }}">
|
||||
<link rel="stylesheet" href="{{ r }}/.cpr/md.css?_={{ ts }}">
|
||||
{%- if edit %}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<title>📝 {{ title }}</title>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=0.7">
|
||||
<meta name="theme-color" content="#333">
|
||||
<meta name="theme-color" content="#{{ tcolor }}">
|
||||
<link rel="stylesheet" href="{{ r }}/.cpr/ui.css?_={{ ts }}">
|
||||
<link rel="stylesheet" href="{{ r }}/.cpr/mde.css?_={{ ts }}">
|
||||
<link rel="stylesheet" href="{{ r }}/.cpr/deps/mini-fa.css?_={{ ts }}">
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<title>{{ s_doctitle }}</title>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=0.8">
|
||||
<meta name="theme-color" content="#333">
|
||||
<meta name="theme-color" content="#{{ tcolor }}">
|
||||
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/msg.css?_={{ ts }}">
|
||||
{{ html_head }}
|
||||
</head>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<title>{{ s_doctitle }}</title>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=0.8">
|
||||
<meta name="theme-color" content="#333">
|
||||
<meta name="theme-color" content="#{{ tcolor }}">
|
||||
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/splash.css?_={{ ts }}">
|
||||
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/ui.css?_={{ ts }}">
|
||||
{{ html_head }}
|
||||
@@ -95,6 +95,7 @@
|
||||
<form method="post" enctype="multipart/form-data" action="{{ r }}/{{ qvpath }}">
|
||||
<input type="hidden" name="act" value="login" />
|
||||
<input type="password" name="cppwd" placeholder=" password" />
|
||||
<input type="hidden" name="uhash" id="uhash" value="x" />
|
||||
<input type="submit" value="Login" />
|
||||
{% if ahttps %}
|
||||
<a id="w" href="{{ ahttps }}">switch to https</a>
|
||||
|
||||
@@ -34,8 +34,14 @@ var Ls = {
|
||||
"u2": "time since the last server write$N( upload / rename / ... )$N$N17d = 17 days$N1h23 = 1 hour 23 minutes$N4m56 = 4 minutes 56 seconds",
|
||||
"v2": "use this server as a local HDD$N$NWARNING: this will show your password!",
|
||||
}
|
||||
},
|
||||
d = Ls[sread("cpp_lang", ["eng", "nor"]) || lang] || Ls.eng || Ls.nor;
|
||||
};
|
||||
|
||||
var LANGS = ["eng", "nor"];
|
||||
|
||||
if (window.langmod)
|
||||
langmod();
|
||||
|
||||
var d = Ls[sread("cpp_lang", LANGS) || lang] || Ls.eng || Ls.nor;
|
||||
|
||||
for (var k in (d || {})) {
|
||||
var f = k.slice(-1),
|
||||
@@ -66,3 +72,5 @@ if (!ebi('c') && o.offsetTop + o.offsetHeight < window.innerHeight)
|
||||
o = ebi('u');
|
||||
if (o && /[0-9]+$/.exec(o.innerHTML))
|
||||
o.innerHTML = shumantime(o.innerHTML);
|
||||
|
||||
ebi('uhash').value = '' + location.hash;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<title>{{ s_doctitle }}</title>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=0.8">
|
||||
<meta name="theme-color" content="#333">
|
||||
<meta name="theme-color" content="#{{ tcolor }}">
|
||||
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/splash.css?_={{ ts }}">
|
||||
<link rel="stylesheet" media="screen" href="{{ r }}/.cpr/ui.css?_={{ ts }}">
|
||||
<style>ul{padding-left:1.3em}li{margin:.4em 0}</style>
|
||||
|
||||
@@ -1539,6 +1539,8 @@ var modal = (function () {
|
||||
cb_up = null,
|
||||
cb_ok = null,
|
||||
cb_ng = null,
|
||||
sel_0 = 0,
|
||||
sel_1 = 0,
|
||||
tok, tng, prim, sec, ok_cancel;
|
||||
|
||||
r.load = function () {
|
||||
@@ -1572,7 +1574,7 @@ var modal = (function () {
|
||||
(inp || a).focus();
|
||||
if (inp)
|
||||
setTimeout(function () {
|
||||
inp.setSelectionRange(0, inp.value.length, "forward");
|
||||
inp.setSelectionRange(sel_0, sel_1, "forward");
|
||||
}, 0);
|
||||
|
||||
document.addEventListener('focus', onfocus);
|
||||
@@ -1695,16 +1697,18 @@ var modal = (function () {
|
||||
r.show(html);
|
||||
}
|
||||
|
||||
r.prompt = function (html, v, cok, cng, fun) {
|
||||
r.prompt = function (html, v, cok, cng, fun, so0, so1) {
|
||||
q.push(function () {
|
||||
_prompt(lf2br(html), v, cok, cng, fun);
|
||||
_prompt(lf2br(html), v, cok, cng, fun, so0, so1);
|
||||
});
|
||||
next();
|
||||
}
|
||||
var _prompt = function (html, v, cok, cng, fun) {
|
||||
var _prompt = function (html, v, cok, cng, fun, so0, so1) {
|
||||
cb_ok = cok;
|
||||
cb_ng = cng === undefined ? cok : null;
|
||||
cb_up = fun;
|
||||
sel_0 = so0 || 0;
|
||||
sel_1 = so1 === undefined ? v.length : so1;
|
||||
html += '<input id="modali" type="text" ' + NOAC + ' /><div id="modalb">' + ok_cancel + '</div>';
|
||||
r.show(html);
|
||||
|
||||
|
||||
@@ -1,3 +1,58 @@
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2024-0506-0029 `v1.13.1` ctrl-v
|
||||
|
||||
## new features
|
||||
|
||||
* upload files by `ctrl-c` from OS and `ctrl-v` into browser c5f7cfc3
|
||||
* from just about any file manager (windows explorer, thunar on linux, etc.) into the copyparty web-ui
|
||||
* only files, not folders, so drag-drop is still the recommended way
|
||||
* empty folders show an "empty folder" banner fdda567f
|
||||
* opengraph / discord embeds ea270ab9 36f2c446 48a6789d b15a4ef7
|
||||
* embeds [audio with covers](https://cd.ocv.me/c/d2/d22/snowy.mp3) , [images](https://cd.ocv.me/c/d2/d22/cover.jpg) , [videos](https://cd.ocv.me/c/d2/d21/no-effect.webm) , [audio without coverart](https://cd.ocv.me/c/d2/bitconnect.mp3) (links to one of the copyparty demoservers where the feature is enabled; link those in discord to test)
|
||||
* images are currently not rendering correctly once clicked on android-discord (works on ios and in browser)
|
||||
* default-disabled because opengraph disables hotlinking by design
|
||||
* enable with `--og` and [see readme](https://github.com/9001/copyparty#opengraph) and [the --help](https://github.com/9001/copyparty/assets/241032/2dabf21e-2470-4e20-8ef0-3821b24be1b6)
|
||||
* add option to support base64-encoded url queries parceled into the url location 69517e46
|
||||
* because android-specific discord bugs prevent the use of queries in opengraph tags
|
||||
* improve server performance when downloading unfinished uploads, especially on slow storage 70a3cf36
|
||||
* add dynamic content into `<head>` using `--html-head` which now takes files and/or jinja templates as input b6cf2d30
|
||||
* `--au-vol` (default 50, same as before) sets default audio volume in percent da091aec
|
||||
* add **[copyparty.pyz](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py)** buildscript 27485a4c
|
||||
* support ie4 and the [version of winzip](https://a.ocv.me/pub/g/nerd-stuff/cpp/win311zip.png) you'd find on an average windows 3.11 pc 603d0ed7
|
||||
|
||||
## bugfixes
|
||||
|
||||
* when logging in from the 403 page, remember and apply the original url hash f8491970
|
||||
* the config-reset button in the control-panel didn't clear the dotfiles preference bc2c1e42
|
||||
* the search feature could discover and use stale indexes in volumes where indexing was since disabled 95d9e693
|
||||
* when in doubt, periodically recheck if filesystems support sparse files f6e693f0
|
||||
* reduces opportunities for confusion on servers with removable media (usb flashdrives)
|
||||
|
||||
----
|
||||
|
||||
this release introduces **[copyparty.pyz](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py)**, yet another way to bring copyparty where it's needed -- very limited and with many drawbacks (see [readme](https://github.com/9001/copyparty#zipapp)) but may work when the others don't
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2024-0420-2232 `v1.13.0` race the beam
|
||||
|
||||
## new features
|
||||
|
||||
* files can be downloaded before the upload has completed ("almost like peer-to-peer")
|
||||
* watch the [release trailer](http://a.ocv.me/pub/g/nerd-stuff/cpp/2024-0418-race-the-beam.webm) 👌
|
||||
* if the downloader catches up with the upload, the speed is gradually slowed down so it never runs ahead
|
||||
* can be disabled with `--no-pipe`
|
||||
* option `--no-db-ip` disables storing the uploader IP in the database bf585078
|
||||
* u2c (cli uploader): option `--ow` to overwrite existing files on the server 439cb7f8
|
||||
|
||||
## bugfixes
|
||||
|
||||
* when running on windows, using the web-UI to abort an upload could fail 8c552f1a
|
||||
* rapidly PUT-uploading and then deleting files could crash the file hasher feecb3e0
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2024-0412-2110 `v1.12.2` ie11 fix
|
||||
|
||||
|
||||
@@ -134,6 +134,9 @@ authenticate using header `Cookie: cppwd=foo` or url param `&pw=foo`
|
||||
| GET | `?zip=utf-8` | ...as a zip file |
|
||||
| GET | `?zip` | ...as a WinXP-compatible zip file |
|
||||
| GET | `?zip=crc` | ...as an MSDOS-compatible zip file |
|
||||
| GET | `?tar&w` | pregenerate webp thumbnails |
|
||||
| GET | `?tar&j` | pregenerate jpg thumbnails |
|
||||
| GET | `?tar&p` | pregenerate audio waveforms |
|
||||
| GET | `?ups` | show recent uploads from your IP |
|
||||
| GET | `?ups&filter=f` | ...where URL contains `f` |
|
||||
| GET | `?mime=foo` | specify return mimetype `foo` |
|
||||
|
||||
@@ -46,7 +46,7 @@ open up notepad and save the following as `c:\users\you\documents\party.conf` (f
|
||||
|
||||
### config explained: [global]
|
||||
|
||||
the `[global]` section accepts any config parameters you can see when running copyparty (either the exe or the sfx.py) with `--help`, so this is the same as running copyparty with arguments `--lo c:\users\you\logs\copyparty-%Y-%m%d.xz -e2dsa -e2ts --no-dedup -z -p 80,443 --theme 2 --lang nor`
|
||||
the `[global]` section accepts any config parameters [listed here](https://ocv.me/copyparty/helptext.html), also viewable by running copyparty (either the exe or the sfx.py) with `--help`, so this is the same as running copyparty with arguments `--lo c:\users\you\logs\copyparty-%Y-%m%d.xz -e2dsa -e2ts --no-dedup -z -p 80,443 --theme 2 --lang nor`
|
||||
* `lo: ~/logs/cpp-%Y-%m%d.xz` writes compressed logs (the compression will make them delayed)
|
||||
* `e2dsa` enables the upload deduplicator and file indexer, which enables searching
|
||||
* `e2ts` enables music metadata indexing, making albums / titles etc. searchable too
|
||||
|
||||
@@ -221,6 +221,11 @@ sox -DnV -r8000 -b8 -c1 /dev/shm/a.wav synth 1.1 sin 400 vol 0.02
|
||||
# play icon calibration pics
|
||||
for w in 150 170 190 210 230 250; do for h in 130 150 170 190 210; do /c/Program\ Files/ImageMagick-7.0.11-Q16-HDRI/magick.exe convert -size ${w}x${h} xc:brown -fill orange -draw "circle $((w/2)),$((h/2)) $((w/2)),$((h/3))" $w-$h.png; done; done
|
||||
|
||||
# compress chiptune modules
|
||||
mkdir gz; for f in *.*; do pigz -c11 -I100 <"$f" >gz/"$f"gz; touch -r "$f" gz/"$f"gz; done
|
||||
mkdir xz; for f in *.*; do xz -cz9 <"$f" >xz/"$f"xz; touch -r "$f" xz/"$f"xz; done
|
||||
mkdir z; for f in *.*; do 7z a -tzip -mx=9 -mm=lzma "z/${f}z" "$f" && touch -r "$f" z/"$f"z; done
|
||||
|
||||
|
||||
##
|
||||
## vscode
|
||||
|
||||
@@ -47,3 +47,25 @@ and if you want to have a monospace font in the fancy markdown editor, do this:
|
||||
|
||||
NB: `<textarea id="mt">` and `<div id="mtr">` in the regular markdown editor must have the same font; none of the suggestions above will cause any issues but keep it in mind if you're getting creative
|
||||
|
||||
|
||||
# `<head>`
|
||||
|
||||
to add stuff to the html `<head>`, for example a css `<link>` or `<meta>` tags, use either the global-option `--html-head` or the volflag `html_head`
|
||||
|
||||
if you give it the value `@ASDF` it will try to open a file named ASDF and send the text within
|
||||
|
||||
if the value starts with `%` it will assume a jinja2 template and expand it; the template has access to the `HttpCli` object through a property named `this` as well as everything in `j2a` and the stuff added by `self.j2s`; see [browser.html](https://github.com/9001/copyparty/blob/hovudstraum/copyparty/web/browser.html) for inspiration or look under the hood in [httpcli.py](https://github.com/9001/copyparty/blob/hovudstraum/copyparty/httpcli.py)
|
||||
|
||||
|
||||
# translations
|
||||
|
||||
add your own translations by using the english or norwegian one from `browser.js` as a template
|
||||
|
||||
the easy way is to open up and modify `browser.js` in your own installation; depending on how you installed copyparty it might be named `browser.js.gz` instead, in which case just decompress it, restart copyparty, and start editing it anyways
|
||||
|
||||
if you're running `copyparty-sfx.py` then you'll find it at `/tmp/pe-copyparty.1000/copyparty/web` (on linux) or `%TEMP%\pe-copyparty\copyparty\web` (on windows)
|
||||
* make sure to keep backups of your work religiously! since that location is volatile af
|
||||
|
||||
if editing `browser.js` is inconvenient in your setup then you can instead do this:
|
||||
* add your translation to a separate javascript file (`tl.js`) and make it load before `browser.js` with the help of `--html-head='<script src="/tl.js"></script>'`
|
||||
* as the page loads, `browser.js` will look for a function named `langmod` so define that function and make it insert your translation into the `Ls` and `LANGS` variables so it'll take effect
|
||||
|
||||
@@ -3,7 +3,7 @@ WORKDIR /z
|
||||
ENV ver_asmcrypto=c72492f4a66e17a0e5dd8ad7874de354f3ccdaa5 \
|
||||
ver_hashwasm=4.10.0 \
|
||||
ver_marked=4.3.0 \
|
||||
ver_dompf=3.0.11 \
|
||||
ver_dompf=3.1.2 \
|
||||
ver_mde=2.18.0 \
|
||||
ver_codemirror=5.65.16 \
|
||||
ver_fontawesome=5.13.0 \
|
||||
|
||||
61
scripts/make-pyz.sh
Executable file
61
scripts/make-pyz.sh
Executable file
@@ -0,0 +1,61 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
echo
|
||||
|
||||
# port install gnutar gsed coreutils
|
||||
gtar=$(command -v gtar || command -v gnutar) || true
|
||||
[ ! -z "$gtar" ] && command -v gsed >/dev/null && {
|
||||
tar() { $gtar "$@"; }
|
||||
sed() { gsed "$@"; }
|
||||
command -v grealpath >/dev/null &&
|
||||
realpath() { grealpath "$@"; }
|
||||
}
|
||||
|
||||
targs=(--owner=1000 --group=1000)
|
||||
[ "$OSTYPE" = msys ] &&
|
||||
targs=()
|
||||
|
||||
[ -e copyparty/__main__.py ] || cd ..
|
||||
[ -e copyparty/__main__.py ] || {
|
||||
echo "run me from within the project root folder"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
[ -e sfx/copyparty/__main__.py ] || {
|
||||
echo "run ./scripts/make-sfx.py first"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
rm -rf pyz
|
||||
mkdir -p pyz
|
||||
cd pyz
|
||||
|
||||
cp -pR ../sfx/{copyparty,partftpy} .
|
||||
cp -pR ../sfx/{ftp,j2}/* .
|
||||
|
||||
ts=$(date -u +%s)
|
||||
hts=$(date -u +%Y-%m%d-%H%M%S)
|
||||
ver="$(cat ../sfx/ver)"
|
||||
|
||||
mkdir -p ../dist
|
||||
pyz_out=../dist/copyparty.pyz
|
||||
|
||||
echo creating z.tar
|
||||
( cd copyparty
|
||||
tar -cf z.tar "${targs[@]}" --numeric-owner web res
|
||||
rm -rf web res
|
||||
)
|
||||
|
||||
echo creating loader
|
||||
sed -r 's/^(VER = ).*/\1"'"$ver"'"/; s/^(STAMP = ).*/\1'$(date +%s)/ \
|
||||
<../scripts/ziploader.py \
|
||||
>__main__.py
|
||||
|
||||
echo creating pyz
|
||||
rm -f $pyz_out
|
||||
zip -9 -q -r $pyz_out *
|
||||
|
||||
echo done:
|
||||
echo " $(realpath $pyz_out)"
|
||||
@@ -99,9 +99,6 @@ pybin=$(command -v python3 || command -v python) || {
|
||||
exit 1
|
||||
}
|
||||
|
||||
[ $CSN ] ||
|
||||
CSN=sfx
|
||||
|
||||
langs=
|
||||
use_gz=
|
||||
zopf=2560
|
||||
@@ -148,9 +145,9 @@ stamp=$(
|
||||
done | sort | tail -n 1 | sha1sum | cut -c-16
|
||||
)
|
||||
|
||||
rm -rf $CSN/*
|
||||
mkdir -p $CSN build
|
||||
cd $CSN
|
||||
rm -rf sfx$CSN/*
|
||||
mkdir -p sfx$CSN build
|
||||
cd sfx$CSN
|
||||
|
||||
tmpdir="$(
|
||||
printf '%s\n' "$TMPDIR" /tmp |
|
||||
@@ -386,11 +383,13 @@ git describe --tags >/dev/null 2>/dev/null && {
|
||||
ver="$(awk '/^VERSION *= \(/ {
|
||||
gsub(/[^0-9,a-g-]/,""); gsub(/,/,"."); print; exit}' < copyparty/__version__.py)"
|
||||
|
||||
echo "$ver" >ver # pyz
|
||||
|
||||
ts=$(date -u +%s)
|
||||
hts=$(date -u +%Y-%m%d-%H%M%S) # --date=@$ts (thx osx)
|
||||
|
||||
mkdir -p ../dist
|
||||
sfx_out=../dist/copyparty-$CSN
|
||||
sfx_out=../dist/copyparty-sfx$CSN
|
||||
|
||||
echo cleanup
|
||||
find -name '*.pyc' -delete
|
||||
@@ -542,7 +541,7 @@ gzres() {
|
||||
}
|
||||
|
||||
|
||||
zdir="$tmpdir/cpp-mk$CSN"
|
||||
zdir="$tmpdir/cpp-mksfx$CSN"
|
||||
[ -e "$zdir/$stamp" ] || rm -rf "$zdir"
|
||||
mkdir -p "$zdir"
|
||||
echo a > "$zdir/$stamp"
|
||||
|
||||
@@ -24,7 +24,7 @@ f298e34356b5590dde7477d7b3a88ad39c622a2bcf3fcd7c53870ce8384dd510f690af81b8f42e12
|
||||
479a63e14586ab2f2228208116fc149ed8ee7b1e4ff360754f5bda4bf765c61af2e04b5ef123976623d04df4976b7886e0445647269da81436bd0a7b5671d361 windows6.1-kb2533623-x86.msu
|
||||
ac96786e5d35882e0c5b724794329c9125c2b86ae7847f17acfc49f0d294312c6afc1c3f248655de3f0ccb4ca426d7957d02ba702f4a15e9fcd7e2c314e72c19 zipp-3.15.0-py3-none-any.whl
|
||||
# win10
|
||||
e3e2e6bd511dec484dd0292f4c46c55c88a885eabf15413d53edea2dd4a4dbae1571735b9424f78c0cd7f1082476a8259f31fd3f63990f726175470f636df2b3 Jinja2-3.1.3-py3-none-any.whl
|
||||
d1420c8417fad7888766dd26b9706a87c63e8f33dceeb8e26d0056d5127b0b3ed9272e44b4b761132d4b3320327252eab1696520488ca64c25958896b41f547b jinja2-3.1.4-py3-none-any.whl
|
||||
e21495f1d473d855103fb4a243095b498ec90eb68776b0f9b48e994990534f7286c0292448e129c507e5d70409f8a05cca58b98d59ce2a815993d0a873dfc480 MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl
|
||||
8a6e2b13a2ec4ef914a5d62aad3db6464d45e525a82e07f6051ed10474eae959069e165dba011aefb8207cdfd55391d73d6f06362c7eb247b08763106709526e mutagen-1.47.0-py3-none-any.whl
|
||||
1dfe6f66bef5c9d62c9028a964196b902772ec9e19db215f3f41acb8d2d563586988d81b455fa6b895b434e9e1e9d57e4d271d1b1214483bdb3eadffcbba6a33 pillow-10.3.0-cp311-cp311-win_amd64.whl
|
||||
|
||||
@@ -24,7 +24,7 @@ fns=(
|
||||
)
|
||||
[ $w10 ] && fns+=(
|
||||
pyinstaller-6.5.0-py3-none-win_amd64.whl
|
||||
Jinja2-3.1.3-py3-none-any.whl
|
||||
Jinja2-3.1.4-py3-none-any.whl
|
||||
MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl
|
||||
mutagen-1.47.0-py3-none-any.whl
|
||||
pillow-10.3.0-cp311-cp311-win_amd64.whl
|
||||
|
||||
@@ -42,7 +42,7 @@ $f$s.py --version >/dev/null
|
||||
min=99999999
|
||||
for ((a=0; a<$parallel; a++)); do
|
||||
while [ -e .sfx-run ]; do
|
||||
CSN=sfx$a ./make-sfx.sh re "$@"
|
||||
CSN=$a ./make-sfx.sh re "$@"
|
||||
sz=$(wc -c <$f$a$s.py | awk '{print$1}')
|
||||
[ $sz -ge $min ] && continue
|
||||
mv $f$a$s.py $f$s.py.$sz
|
||||
|
||||
@@ -1,6 +1,21 @@
|
||||
#!/bin/bash
|
||||
set -ex
|
||||
|
||||
if uname | grep -iE '^(msys|mingw)'; then
|
||||
pids=()
|
||||
|
||||
python -m unittest discover -s tests >/dev/null &
|
||||
pids+=($!)
|
||||
|
||||
python scripts/test/smoketest.py &
|
||||
pids+=($!)
|
||||
|
||||
for pid in ${pids[@]}; do
|
||||
wait $pid
|
||||
done
|
||||
exit $?
|
||||
fi
|
||||
|
||||
# osx support
|
||||
gtar=$(command -v gtar || command -v gnutar) || true
|
||||
[ ! -z "$gtar" ] && command -v gfind >/dev/null && {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
# coding: latin-1
|
||||
from __future__ import print_function, unicode_literals
|
||||
import re, os, sys, time, shutil, signal, threading, tarfile, hashlib, platform, tempfile, traceback
|
||||
import re, os, sys, time, shutil, signal, tarfile, hashlib, platform, tempfile, traceback
|
||||
import subprocess as sp
|
||||
|
||||
|
||||
@@ -368,17 +368,6 @@ def get_payload():
|
||||
p = a
|
||||
|
||||
|
||||
def utime(top):
|
||||
# avoid cleaners
|
||||
files = [os.path.join(dp, p) for dp, dd, df in os.walk(top) for p in dd + df]
|
||||
while True:
|
||||
t = int(time.time())
|
||||
for f in [top] + files:
|
||||
os.utime(f, (t, t))
|
||||
|
||||
time.sleep(78123)
|
||||
|
||||
|
||||
def confirm(rv):
|
||||
msg()
|
||||
msg("retcode", rv if rv else traceback.format_exc())
|
||||
@@ -398,9 +387,7 @@ def run(tmp, j2, ftp):
|
||||
msg("sfxdir:", tmp)
|
||||
msg()
|
||||
|
||||
t = threading.Thread(target=utime, args=(tmp,))
|
||||
t.daemon = True
|
||||
t.start()
|
||||
sys.argv.append("--sfx-tpoke=" + tmp)
|
||||
|
||||
ld = (("", ""), (j2, "j2"), (ftp, "ftp"), (not PY2, "py2"), (PY37, "py37"))
|
||||
ld = [os.path.join(tmp, b) for a, b in ld if not a]
|
||||
|
||||
109
scripts/ziploader.py
Normal file
109
scripts/ziploader.py
Normal file
@@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import atexit
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
import tarfile
|
||||
import tempfile
|
||||
import time
|
||||
import traceback
|
||||
|
||||
|
||||
VER = None
|
||||
STAMP = None
|
||||
WINDOWS = sys.platform in ["win32", "msys"]
|
||||
|
||||
|
||||
def msg(*a, **ka):
|
||||
if a:
|
||||
a = ["[ZIP]", a[0]] + list(a[1:])
|
||||
|
||||
ka["file"] = sys.stderr
|
||||
print(*a, **ka)
|
||||
|
||||
|
||||
def utime(top):
|
||||
# avoid cleaners
|
||||
files = [os.path.join(dp, p) for dp, dd, df in os.walk(top) for p in dd + df]
|
||||
try:
|
||||
while True:
|
||||
t = int(time.time())
|
||||
for f in [top] + files:
|
||||
os.utime(f, (t, t))
|
||||
|
||||
time.sleep(78123)
|
||||
except Exception as ex:
|
||||
print("utime:", ex, f)
|
||||
|
||||
|
||||
def confirm(rv):
|
||||
msg()
|
||||
msg("retcode", rv if rv else traceback.format_exc())
|
||||
if WINDOWS:
|
||||
msg("*** hit enter to exit ***")
|
||||
try:
|
||||
input()
|
||||
except:
|
||||
pass
|
||||
|
||||
sys.exit(rv or 1)
|
||||
|
||||
|
||||
def run():
|
||||
import copyparty
|
||||
from copyparty.__main__ import main as cm
|
||||
|
||||
td = tempfile.TemporaryDirectory(prefix="")
|
||||
atexit.register(td.cleanup)
|
||||
rsrc = td.name
|
||||
|
||||
try:
|
||||
from importlib.resources import files
|
||||
|
||||
f = files(copyparty).joinpath("z.tar").open("rb")
|
||||
except:
|
||||
from importlib.resources import open_binary
|
||||
|
||||
f = open_binary("copyparty", "z.tar")
|
||||
|
||||
with tarfile.open(fileobj=f) as tf:
|
||||
try:
|
||||
tf.extractall(rsrc, filter="tar")
|
||||
except TypeError:
|
||||
tf.extractall(rsrc) # nosec (archive is safe)
|
||||
|
||||
f.close()
|
||||
f = None
|
||||
|
||||
msg(" rsrc dir:", rsrc)
|
||||
msg()
|
||||
|
||||
sys.argv.append("--sfx-tpoke=" + rsrc)
|
||||
|
||||
cm(rsrc=rsrc)
|
||||
|
||||
|
||||
def main():
|
||||
sysver = str(sys.version).replace("\n", "\n" + " " * 18)
|
||||
pktime = time.strftime("%Y-%m-%d, %H:%M:%S", time.gmtime(STAMP))
|
||||
msg()
|
||||
msg(" this is: copyparty", VER)
|
||||
msg(" packed at:", pktime, "UTC,", STAMP)
|
||||
msg("python bin:", sys.executable)
|
||||
msg("python ver:", platform.python_implementation(), sysver)
|
||||
|
||||
try:
|
||||
run()
|
||||
except SystemExit as ex:
|
||||
c = ex.code
|
||||
if c not in [0, -15]:
|
||||
confirm(ex.code)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
except:
|
||||
confirm(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -80,7 +80,8 @@ class TestHttpCli(unittest.TestCase):
|
||||
x = " ".join(sorted([x["rp"] for x in x[0]]))
|
||||
self.assertEqual(x, ".f0 .t/.f2 .t/f2 a/da/f4 a/f3 f0 t/.f1 t/f1")
|
||||
|
||||
self.args = Cfg(v=vcfg, a=["u1:u1", "u2:u2"], dotsrch=False)
|
||||
u2idx.shutdown()
|
||||
self.args = Cfg(v=vcfg, a=["u1:u1", "u2:u2"], dotsrch=False, e2d=True)
|
||||
self.asrv = AuthSrv(self.args, self.log)
|
||||
u2idx = U2idx(self)
|
||||
|
||||
@@ -89,6 +90,8 @@ class TestHttpCli(unittest.TestCase):
|
||||
# u1 can see dotfiles in volB so they should be included
|
||||
xe = "a/da/f4 a/f3 f0 t/f1"
|
||||
self.assertEqual(x, xe)
|
||||
u2idx.shutdown()
|
||||
up2k.shutdown()
|
||||
|
||||
##
|
||||
## dirkeys
|
||||
@@ -147,4 +150,4 @@ class TestHttpCli(unittest.TestCase):
|
||||
return conn.s._reply.decode("utf-8").split("\r\n\r\n", 1)
|
||||
|
||||
def log(self, src, msg, c=0):
|
||||
print(msg)
|
||||
print(msg, "\033[0m")
|
||||
|
||||
@@ -6,6 +6,7 @@ import json
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from copyparty.__init__ import ANYWIN
|
||||
from copyparty.authsrv import AuthSrv
|
||||
from tests.util import Cfg
|
||||
|
||||
@@ -51,6 +52,12 @@ class TestVFS(unittest.TestCase):
|
||||
vn = self.nav(au, vp)
|
||||
self.assertNodes(vn, expected)
|
||||
|
||||
def assertApEq(self, ap, rhs):
|
||||
if ANYWIN and len(ap) > 2 and ap[1] == ":":
|
||||
ap = ap[2:].replace("\\", "/")
|
||||
|
||||
return self.assertEqual(ap, rhs)
|
||||
|
||||
def prep(self):
|
||||
here = os.path.abspath(os.path.dirname(__file__))
|
||||
cfgdir = os.path.join(here, "res", "idp")
|
||||
@@ -70,7 +77,7 @@ class TestVFS(unittest.TestCase):
|
||||
au = AuthSrv(Cfg(c=[cfgdir + "/1.conf"], **xcfg), self.log)
|
||||
|
||||
self.assertEqual(au.vfs.vpath, "")
|
||||
self.assertEqual(au.vfs.realpath, "/")
|
||||
self.assertApEq(au.vfs.realpath, "/")
|
||||
self.assertNodes(au.vfs, ["vb"])
|
||||
self.assertNodes(au.vfs.nodes["vb"], [])
|
||||
|
||||
@@ -85,7 +92,7 @@ class TestVFS(unittest.TestCase):
|
||||
au = AuthSrv(Cfg(c=[cfgdir + "/2.conf"], **xcfg), self.log)
|
||||
|
||||
self.assertEqual(au.vfs.vpath, "")
|
||||
self.assertEqual(au.vfs.realpath, "/")
|
||||
self.assertApEq(au.vfs.realpath, "/")
|
||||
self.assertNodes(au.vfs, ["vb", "vc"])
|
||||
self.assertNodes(au.vfs.nodes["vb"], [])
|
||||
self.assertNodes(au.vfs.nodes["vc"], [])
|
||||
@@ -103,7 +110,7 @@ class TestVFS(unittest.TestCase):
|
||||
au = AuthSrv(Cfg(c=[cfgdir + "/3.conf"], **xcfg), self.log)
|
||||
|
||||
self.assertEqual(au.vfs.vpath, "")
|
||||
self.assertEqual(au.vfs.realpath, "")
|
||||
self.assertApEq(au.vfs.realpath, "")
|
||||
self.assertNodes(au.vfs, [])
|
||||
self.assertAxs(au.vfs.axs, [])
|
||||
|
||||
@@ -112,8 +119,8 @@ class TestVFS(unittest.TestCase):
|
||||
self.assertNodesAt(au, "vu", ["iua"]) # same as:
|
||||
self.assertNodes(au.vfs.nodes["vu"], ["iua"])
|
||||
self.assertNodes(au.vfs.nodes["vg"], ["iga"])
|
||||
self.assertEqual(au.vfs.nodes["vu"].realpath, "")
|
||||
self.assertEqual(au.vfs.nodes["vg"].realpath, "")
|
||||
self.assertApEq(au.vfs.nodes["vu"].realpath, "")
|
||||
self.assertApEq(au.vfs.nodes["vg"].realpath, "")
|
||||
self.assertAxs(au.vfs.axs, [])
|
||||
self.assertAxsAt(au, "vu/iua", [["iua"]]) # same as:
|
||||
self.assertAxs(self.nav(au, "vu/iua").axs, [["iua"]])
|
||||
@@ -127,7 +134,7 @@ class TestVFS(unittest.TestCase):
|
||||
au = AuthSrv(Cfg(c=[cfgdir + "/4.conf"], **xcfg), self.log)
|
||||
|
||||
self.assertEqual(au.vfs.vpath, "")
|
||||
self.assertEqual(au.vfs.realpath, "")
|
||||
self.assertApEq(au.vfs.realpath, "")
|
||||
self.assertNodes(au.vfs, ["vu"])
|
||||
self.assertNodesAt(au, "vu", ["ua", "ub"])
|
||||
self.assertAxs(au.vfs.axs, [])
|
||||
@@ -147,10 +154,10 @@ class TestVFS(unittest.TestCase):
|
||||
self.assertAxsAt(au, "vg", [])
|
||||
self.assertAxsAt(au, "vg/iga1", [["iua"]])
|
||||
self.assertAxsAt(au, "vg/iga2", [["iua", "ua"]])
|
||||
self.assertEqual(self.nav(au, "vu/ua").realpath, "/u-ua")
|
||||
self.assertEqual(self.nav(au, "vu/iua").realpath, "/u-iua")
|
||||
self.assertEqual(self.nav(au, "vg/iga1").realpath, "/g1-iga")
|
||||
self.assertEqual(self.nav(au, "vg/iga2").realpath, "/g2-iga")
|
||||
self.assertApEq(self.nav(au, "vu/ua").realpath, "/u-ua")
|
||||
self.assertApEq(self.nav(au, "vu/iua").realpath, "/u-iua")
|
||||
self.assertApEq(self.nav(au, "vg/iga1").realpath, "/g1-iga")
|
||||
self.assertApEq(self.nav(au, "vg/iga2").realpath, "/g2-iga")
|
||||
|
||||
au.idp_checkin(None, "iub", "iga")
|
||||
self.assertAxsAt(au, "vu/iua", [["iua"]])
|
||||
@@ -165,7 +172,7 @@ class TestVFS(unittest.TestCase):
|
||||
au = AuthSrv(Cfg(c=[cfgdir + "/5.conf"], **xcfg), self.log)
|
||||
|
||||
self.assertEqual(au.vfs.vpath, "")
|
||||
self.assertEqual(au.vfs.realpath, "")
|
||||
self.assertApEq(au.vfs.realpath, "")
|
||||
self.assertNodes(au.vfs, ["g", "ga", "gb"])
|
||||
self.assertAxs(au.vfs.axs, [])
|
||||
|
||||
@@ -196,7 +203,7 @@ class TestVFS(unittest.TestCase):
|
||||
|
||||
self.assertAxs(au.vfs.axs, [])
|
||||
self.assertEqual(au.vfs.vpath, "")
|
||||
self.assertEqual(au.vfs.realpath, "")
|
||||
self.assertApEq(au.vfs.realpath, "")
|
||||
self.assertNodes(au.vfs, [])
|
||||
|
||||
au.idp_checkin(None, "iua", "")
|
||||
|
||||
@@ -110,25 +110,25 @@ class Cfg(Namespace):
|
||||
def __init__(self, a=None, v=None, c=None, **ka0):
|
||||
ka = {}
|
||||
|
||||
ex = "daw dav_auth dav_inf dav_mac dav_rt e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp early_ban ed emp exp force_js getmod grid hardlink ih ihead magic never_symlink nid nih no_acode no_athumb no_dav no_dedup no_del no_dupe no_lifetime no_logues no_mv no_pipe no_readme no_robots no_sb_md no_sb_lg no_scandir no_tarcmp no_thumb no_vthumb no_zip nrand nw q rand smb srch_dbg stats vague_403 vc ver xdev xlink xvol"
|
||||
ex = "daw dav_auth dav_inf dav_mac dav_rt e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp early_ban ed emp exp force_js getmod grid hardlink ih ihead magic never_symlink nid nih no_acode no_athumb no_dav no_dedup no_del no_dupe no_lifetime no_logues no_mv no_pipe no_readme no_robots no_sb_md no_sb_lg no_scandir no_tarcmp no_thumb no_vthumb no_zip nrand nw og og_no_head og_s_title q rand smb srch_dbg stats uqe vague_403 vc ver xdev xlink xvol"
|
||||
ka.update(**{k: False for k in ex.split()})
|
||||
|
||||
ex = "dotpart dotsrch no_dhash no_fastboot no_rescan no_sendfile no_voldump re_dhash plain_ip"
|
||||
ex = "dotpart dotsrch no_dhash no_fastboot no_rescan no_sendfile no_snap no_voldump re_dhash plain_ip"
|
||||
ka.update(**{k: True for k in ex.split()})
|
||||
|
||||
ex = "ah_cli ah_gen css_browser hist js_browser no_forget no_hash no_idx nonsus_urls"
|
||||
ex = "ah_cli ah_gen css_browser hist js_browser mime mimes no_forget no_hash no_idx nonsus_urls og_tpl og_ua"
|
||||
ka.update(**{k: None for k in ex.split()})
|
||||
|
||||
ex = "hash_mt srch_time u2abort u2j"
|
||||
ka.update(**{k: 1 for k in ex.split()})
|
||||
|
||||
ex = "reg_cap s_thead s_tbody th_convt"
|
||||
ex = "au_vol mtab_age reg_cap s_thead s_tbody th_convt"
|
||||
ka.update(**{k: 9 for k in ex.split()})
|
||||
|
||||
ex = "db_act df k304 loris re_maxage rproxy rsp_jtr rsp_slp s_wr_slp snap_wri theme themes turbo"
|
||||
ka.update(**{k: 0 for k in ex.split()})
|
||||
|
||||
ex = "ah_alg bname doctitle exit favico idp_h_usr html_head lg_sbf log_fk md_sbf name textfiles unlist vname R RS SR"
|
||||
ex = "ah_alg bname doctitle exit favico idp_h_usr html_head lg_sbf log_fk md_sbf name og_desc og_site og_th og_title og_title_a og_title_v og_title_i tcolor textfiles unlist vname R RS SR"
|
||||
ka.update(**{k: "" for k in ex.split()})
|
||||
|
||||
ex = "grp on403 on404 xad xar xau xban xbd xbr xbu xiu xm"
|
||||
@@ -260,3 +260,7 @@ class VHttpConn(object):
|
||||
self.u2fh = FHC()
|
||||
|
||||
self.get_u2idx = self.hsrv.get_u2idx
|
||||
|
||||
|
||||
if WINDOWS:
|
||||
os.system("rem")
|
||||
|
||||
Reference in New Issue
Block a user