mirror of
https://github.com/9001/copyparty.git
synced 2025-10-10 10:32:19 -06:00
Compare commits
218 commits
v1.19.3
...
hovudstrau
Author | SHA1 | Date | |
---|---|---|---|
|
dbd8f837e8 | ||
|
20ac117c32 | ||
|
cd3feaac86 | ||
|
f8e19815e1 | ||
|
76e9f23a6d | ||
|
4542ad3c01 | ||
|
50276c0cfa | ||
|
c5f1229685 | ||
|
73ec2d296f | ||
|
a499648291 | ||
|
4bb5baf1b8 | ||
|
efd19af7ca | ||
|
aace711eb9 | ||
|
39bd4e5b58 | ||
|
08ebb0b4c9 | ||
|
656f0a6c39 | ||
|
805a7054e9 | ||
|
01709691f2 | ||
|
41ed559faa | ||
|
a0f8f794a8 | ||
|
fbe5fa582e | ||
|
2248705e1a | ||
|
eb173be4f1 | ||
|
d05a88d2ee | ||
|
09e6f29e5e | ||
|
2ce32e4fb6 | ||
|
9b7f933b78 | ||
|
38cc809822 | ||
|
e9b6e645d3 | ||
|
0f9a239078 | ||
|
0453b7ac53 | ||
|
1bcdf8c9e3 | ||
|
4177c1d9ed | ||
|
171ca985c8 | ||
|
dacc64dd2e | ||
|
31f1b535b2 | ||
|
7fc379abc8 | ||
|
a8f53d5ef0 | ||
|
3f59710294 | ||
|
24e01221c5 | ||
|
daba1ab7bd | ||
|
1bca86c6e1 | ||
|
fc2754cba5 | ||
|
470b504843 | ||
|
435db14798 | ||
|
d08e872062 | ||
|
f91a653bde | ||
|
7d86f39a23 | ||
|
456addf26f | ||
|
4e38e4087e | ||
|
f0ecb08347 | ||
|
1193f9ba6c | ||
|
234eddec90 | ||
|
e3baf932f3 | ||
|
eb5d767b01 | ||
|
ec7418734d | ||
|
a3d9506783 | ||
|
57650a218f | ||
|
983865d96c | ||
|
6f6b70ad04 | ||
|
e187df28f2 | ||
|
df0fa9d1b7 | ||
|
397ed5653b | ||
|
9f46e4dbd7 | ||
|
6912e86747 | ||
|
80ca78516e | ||
|
a493cd6530 | ||
|
c72b62ad86 | ||
|
fdcd92bac8 | ||
|
b00dac997a | ||
|
9d066414c6 | ||
|
5e4ff90b1c | ||
|
deb8a4a86e | ||
|
733e85c040 | ||
|
892a452446 | ||
|
38df223b8f | ||
|
b136a5b042 | ||
|
377eddcd06 | ||
|
549fe33f51 | ||
|
c214a93caa | ||
|
0941fd4ec1 | ||
|
6dbd9901b2 | ||
|
e9ca36fa88 | ||
|
a053a663b4 | ||
|
1923a25879 | ||
|
4cce799012 | ||
|
1460fe97ac | ||
|
ca872c4055 | ||
|
3ddb4c042a | ||
|
15d3c2fbff | ||
|
6a24432019 | ||
|
8f6194fe77 | ||
|
260da2f45c | ||
|
a9e02ce753 | ||
|
70c088aeca | ||
|
280815f158 | ||
|
e1ea9852c6 | ||
|
2ee9c80d3b | ||
|
4b2ff3a196 | ||
|
538a205ce4 | ||
|
6559152882 | ||
|
669b10754d | ||
|
478f1c764e | ||
|
a043d7cfb6 | ||
|
ee5f31908f | ||
|
35326a6fb8 | ||
|
59a0122179 | ||
|
5996a58b20 | ||
|
fd331a545d | ||
|
e7ef31ee33 | ||
|
49ce67e9cd | ||
|
8b66874b85 | ||
|
3a2381ff2d | ||
|
83bd197438 | ||
|
c03b332ec0 | ||
|
17b4f905a7 | ||
|
8f587627e1 | ||
|
14b7e5143f | ||
|
3e97a77784 | ||
|
3f45492725 | ||
|
9c9e4057e0 | ||
|
3d09bec1bb | ||
|
d7887f3d55 | ||
|
ecd18adc3c | ||
|
e2aa8fc1a4 | ||
|
dfd9e007ee | ||
|
5c1a43c711 | ||
|
52438bcc0d | ||
|
e09f3c9e2c | ||
|
25749b4b5f | ||
|
75b0b312a4 | ||
|
c47c708433 | ||
|
e0a92ba72d | ||
|
98386f28f0 | ||
|
422f8f624e | ||
|
edafa1586a | ||
|
e270fe60ed | ||
|
ab56238249 | ||
|
3bdef75e88 | ||
|
67ba5b0252 | ||
|
06d2654b3f | ||
|
1cdb388090 | ||
|
f7e08ed007 | ||
|
b049631169 | ||
|
aaeec11f81 | ||
|
96b109b0d6 | ||
|
74821a38ad | ||
|
19a4c45389 | ||
|
09f22993be | ||
|
c2be664e96 | ||
|
7a4ee4dbc8 | ||
|
bd6d1f961d | ||
|
eeb7738b53 | ||
|
e6755aa8a1 | ||
|
230a146209 | ||
|
c71128fd72 | ||
|
b59b915962 | ||
|
f0caf88185 | ||
|
bfcb6eac41 | ||
|
e798a9a53a | ||
|
09e3018bf9 | ||
|
87539800e8 | ||
|
0469b5a29e | ||
|
3e90abbf6f | ||
|
26a29797a6 | ||
|
14555d5832 | ||
|
d1f75229b5 | ||
|
01cf20a029 | ||
|
6f0871173e | ||
|
914686ec7c | ||
|
0d96786e68 | ||
|
4c3792de07 | ||
|
200eaa92d7 | ||
|
d40f061a79 | ||
|
28b93d7961 | ||
|
aa1c921302 | ||
|
2848941e01 | ||
|
f4f702c39d | ||
|
4c042b3c82 | ||
|
0b50fde305 | ||
|
0b3939002d | ||
|
543b7ea959 | ||
|
d30240b431 | ||
|
599e82f24d | ||
|
abffda5474 | ||
|
35472557cb | ||
|
48d6224ec8 | ||
|
68503444c7 | ||
|
0491123bb2 | ||
|
8f235be66f | ||
|
4b8c22159a | ||
|
59f142cd19 | ||
|
6cd0a396df | ||
|
6413ad3e8d | ||
|
ad0e6c7fde | ||
|
978801d020 | ||
|
d39c74c126 | ||
|
5c250c2c19 | ||
|
ceaf133d9d | ||
|
202ddeac0d | ||
|
cc4f4aef99 | ||
|
f9cb2c15e3 | ||
|
cc65b1b551 | ||
|
6d76254c88 | ||
|
0de07d8e8b | ||
|
ca98d54fda | ||
|
6c76614eb1 | ||
|
63d4ec64cd | ||
|
cd8771fa52 | ||
|
15c5b50a36 | ||
|
7a4973fa56 | ||
|
3259367007 | ||
|
5e36f02595 | ||
|
c51371c71d | ||
|
20ef74cdac | ||
|
b5c6b4fa99 | ||
|
377f7732de | ||
|
b2fb0c26ad |
52
.vscode/launch.json
vendored
52
.vscode/launch.json
vendored
|
@ -3,7 +3,7 @@
|
|||
"configurations": [
|
||||
{
|
||||
"name": "Run copyparty",
|
||||
"type": "python",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"module": "copyparty",
|
||||
"console": "integratedTerminal",
|
||||
|
@ -11,30 +11,46 @@
|
|||
"justMyCode": false,
|
||||
"env": {
|
||||
"PYDEVD_DISABLE_FILE_VALIDATION": "1",
|
||||
"PYTHONWARNINGS": "always", //error
|
||||
"PYTHONWARNINGS": "always" //error
|
||||
},
|
||||
"args": [
|
||||
//"-nw",
|
||||
"-ed",
|
||||
"-emp",
|
||||
//"-nw", // no-write; for testing uploads without writing to disk
|
||||
//"-q", // quiet; speedboost when console output is not needed
|
||||
|
||||
// # increase debugger performance:
|
||||
//"no-htp",
|
||||
//"hash-mt=0",
|
||||
//"mtag-mt=1",
|
||||
//"th-mt=1",
|
||||
|
||||
// # listen for FTP and TFTP
|
||||
"--ftp=3921",
|
||||
"--ftp-pr=12000-12099",
|
||||
"--tftp=3969",
|
||||
|
||||
// # listen on all IPv6, all IPv4, and unix-socket
|
||||
"-i::,unix:777:a.sock",
|
||||
|
||||
// # misc
|
||||
"--dedup",
|
||||
"-e2dsa",
|
||||
"-e2ts",
|
||||
"-mtp=.bpm=f,bin/mtag/audio-bpm.py",
|
||||
"--rss",
|
||||
"--shr=/shr",
|
||||
"--stats",
|
||||
"-z",
|
||||
|
||||
// # users + volumes
|
||||
"-aed:wark",
|
||||
"-vsrv::r:rw,ed:c,dupe",
|
||||
"-vdist:dist:r"
|
||||
"-vdist:dist:r",
|
||||
"-vsrv::r:rw,ed",
|
||||
"-vsrv/junk:junk:r:A,ed",
|
||||
"--ver"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "No debug",
|
||||
"preLaunchTask": "no_dbg",
|
||||
"type": "python",
|
||||
//"request": "attach", "port": 42069
|
||||
// fork: nc -l 42069 </dev/null
|
||||
},
|
||||
{
|
||||
"name": "Run active unit test",
|
||||
"type": "python",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"module": "unittest",
|
||||
"console": "integratedTerminal",
|
||||
|
@ -51,6 +67,6 @@
|
|||
"program": "${file}",
|
||||
"console": "integratedTerminal",
|
||||
"justMyCode": false
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,13 +6,13 @@ but please:
|
|||
|
||||
|
||||
|
||||
# do not use AI / LMM when writing code
|
||||
# do not use AI / LLM when writing code
|
||||
|
||||
copyparty is 100% organic, free-range, human-written software!
|
||||
|
||||
> ⚠ you are now entering a no-copilot zone
|
||||
|
||||
the *only* place where LMM/AI *may* be accepted is for [localization](https://github.com/9001/copyparty/tree/hovudstraum/docs/rice#translations) if you are fluent and have confirmed that the translation is accurate.
|
||||
the *only* place where LLM/AI *may* be accepted is for [localization](https://github.com/9001/copyparty/tree/hovudstraum/docs/rice#translations) if you are fluent and have confirmed that the translation is accurate.
|
||||
|
||||
sorry for the harsh tone, but this is important to me 🙏
|
||||
|
||||
|
|
177
README.md
177
README.md
|
@ -50,6 +50,7 @@ made in Norway 🇳🇴
|
|||
* [shares](#shares) - share a file or folder by creating a temporary link
|
||||
* [batch rename](#batch-rename) - select some files and press `F2` to bring up the rename UI
|
||||
* [rss feeds](#rss-feeds) - monitor a folder with your RSS reader
|
||||
* [opds feeds](#opds-feeds) - browse and download files from your e-book reader
|
||||
* [recent uploads](#recent-uploads) - list all recent uploads
|
||||
* [media player](#media-player) - plays almost every audio format there is
|
||||
* [playlists](#playlists) - create and play [m3u8](https://en.wikipedia.org/wiki/M3U) playlists
|
||||
|
@ -112,6 +113,7 @@ made in Norway 🇳🇴
|
|||
* [packages](#packages) - the party might be closer than you think
|
||||
* [arch package](#arch-package) - `pacman -S copyparty` (in [arch linux extra](https://archlinux.org/packages/extra/any/copyparty/))
|
||||
* [fedora package](#fedora-package) - does not exist yet
|
||||
* [homebrew formulae](#homebrew-formulae) - `brew install copyparty ffmpeg`
|
||||
* [nix package](#nix-package) - `nix profile install github:9001/copyparty`
|
||||
* [nixos module](#nixos-module)
|
||||
* [browser support](#browser-support) - TLDR: yes
|
||||
|
@ -136,11 +138,13 @@ made in Norway 🇳🇴
|
|||
* [dependencies](#dependencies) - mandatory deps
|
||||
* [optional dependencies](#optional-dependencies) - install these to enable bonus features
|
||||
* [dependency chickenbits](#dependency-chickenbits) - prevent loading an optional dependency
|
||||
* [dependency unvendoring](#dependency-unvendoring) - force use of system modules
|
||||
* [optional gpl stuff](#optional-gpl-stuff)
|
||||
* [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)
|
||||
* [install on iOS](#install-on-iOS)
|
||||
* [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)
|
||||
|
||||
|
@ -155,6 +159,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 maybe an iPhone or iPad? [install in a-Shell on iOS](#install-on-iOS)
|
||||
* or maybe you have a [synology nas / dsm](./docs/synology-dsm.md)
|
||||
* or if you have [uv](https://docs.astral.sh/uv/) installed, run `uv tool run copyparty`
|
||||
* or if your computer is messed up and nothing else works, [try the pyz](#zipapp)
|
||||
|
@ -209,6 +214,7 @@ you may also want these, especially on servers:
|
|||
|
||||
* [contrib/systemd/copyparty.service](contrib/systemd/copyparty.service) to run copyparty as a systemd service (see guide inside)
|
||||
* [contrib/systemd/prisonparty.service](contrib/systemd/prisonparty.service) to run it in a chroot (for extra security)
|
||||
* [contrib/podman-systemd/](contrib/podman-systemd/) to run copyparty in a Podman container as a systemd service (see guide inside)
|
||||
* [contrib/openrc/copyparty](contrib/openrc/copyparty) to run copyparty on Alpine / Gentoo
|
||||
* [contrib/rc/copyparty](contrib/rc/copyparty) to run copyparty on FreeBSD
|
||||
* [nixos module](#nixos-module) to run copyparty on NixOS hosts
|
||||
|
@ -241,7 +247,7 @@ also see [comparison to similar software](./docs/versus.md)
|
|||
* ☑ [upnp / zeroconf / mdns / ssdp](#zeroconf)
|
||||
* ☑ [event hooks](#event-hooks) / script runner
|
||||
* ☑ [reverse-proxy support](https://github.com/9001/copyparty#reverse-proxy)
|
||||
* ☑ cross-platform (Windows, Linux, Macos, Android, FreeBSD, arm32/arm64, ppc64le, s390x, risc-v/riscv64)
|
||||
* ☑ cross-platform (Windows, Linux, Macos, Android, iOS, FreeBSD, arm32/arm64, ppc64le, s390x, risc-v/riscv64)
|
||||
* upload
|
||||
* ☑ basic: plain multipart, ie6 support
|
||||
* ☑ [up2k](#uploading): js, resumable, multithreaded
|
||||
|
@ -264,7 +270,7 @@ also see [comparison to similar software](./docs/versus.md)
|
|||
* ☑ play video files as audio (converted on server)
|
||||
* ☑ create and play [m3u8 playlists](#playlists)
|
||||
* ☑ image gallery with webm player
|
||||
* ☑ [textfile browser](#textfile-viewer) with syntax hilighting
|
||||
* ☑ [textfile browser](#textfile-viewer) with syntax highlighting
|
||||
* ☑ realtime streaming of growing files (logfiles and such)
|
||||
* ☑ [thumbnails](#thumbnails)
|
||||
* ☑ ...of images using Pillow, pyvips, or FFmpeg
|
||||
|
@ -569,6 +575,8 @@ for example `-v /mnt::r -v /var/empty:web/certs:r` mounts the server folder `/mn
|
|||
|
||||
the example config file right above this section may explain this better; the first volume `/` is mapped to `/srv` which means http://127.0.0.1:3923/music would try to read `/srv/music` on the server filesystem, but since there's another volume at `/music` mapped to `/mnt/music` then it'll go to `/mnt/music` instead
|
||||
|
||||
> ℹ️ this also works for single files, because files can also be volumes
|
||||
|
||||
|
||||
## dotfiles
|
||||
|
||||
|
@ -723,6 +731,10 @@ to show `/icons/exe.png` and `/icons/elf.gif` as the thumbnail for all `.exe` an
|
|||
* the supported image formats are [jpg, png, gif, webp, ico](https://developer.mozilla.org/en-US/docs/Web/Media/Guides/Formats/Image_types)
|
||||
* be careful with svg; chrome will crash if you have too many unique svg files showing on the same page (the limit is 250 or so) -- showing the same handful of svg files thousands of times is ok however
|
||||
|
||||
note:
|
||||
* heif/heifs/heic/heics images usually require the `libvips` [optional dependency](#optional-dependencies) (available in the `iv` docker image, `withFastThumbnails` in nixos)
|
||||
* technical trivia: FFmpeg has basic support for tiled heic as of v7.0; need `-show_stream_groups` for correct resolution
|
||||
|
||||
config file example:
|
||||
|
||||
```yaml
|
||||
|
@ -831,7 +843,7 @@ the up2k UI is the epitome of polished intuitive experiences:
|
|||
* `[🔎]` switch between upload and [file-search](#file-search) mode
|
||||
* ignore `[🔎]` if you add files by dragging them into the browser
|
||||
|
||||
and then theres the tabs below it,
|
||||
and then there's the tabs below it,
|
||||
* `[ok]` is the files which completed successfully
|
||||
* `[ng]` is the ones that failed / got rejected (already exists, ...)
|
||||
* `[done]` shows a combined list of `[ok]` and `[ng]`, chronological order
|
||||
|
@ -863,6 +875,8 @@ the files will be hashed on the client-side, and each hash is sent to the server
|
|||
files go into `[ok]` if they exist (and you get a link to where it is), otherwise they land in `[ng]`
|
||||
* the main reason filesearch is combined with the uploader is cause the code was too spaghetti to separate it out somewhere else, this is no longer the case but now i've warmed up to the idea too much
|
||||
|
||||
if you have a "wark" (file-identifier/checksum) then you can also search for that in the [🔎] tab by putting `w = kFpDiztbZc8Z1Lzi` in the `raw` field
|
||||
|
||||
|
||||
### unpost
|
||||
|
||||
|
@ -989,6 +1003,8 @@ available functions:
|
|||
* `$lpad(text, length, pad_char)`
|
||||
* `$rpad(text, length, pad_char)`
|
||||
|
||||
two counters are available; `.n.s` is the nth file in the selection, and `.n.d` the nth file in the folder, for example rename-output `file(.n.d).(ext)` gives `file5.bin`, and `beach-$lpad((.n.s),3,0).(ext)` is `beach-017.jpg` and the initial value of each counter can be set in the textboxes underneath the preset dropdown
|
||||
|
||||
so,
|
||||
|
||||
say you have a file named [`meganeko - Eclipse - 07 Sirius A.mp3`](https://www.youtube.com/watch?v=-dtb0vDPruI) (absolutely fantastic album btw) and the tags are: `Album:Eclipse`, `Artist:meganeko`, `Title:Sirius A`, `tn:7`
|
||||
|
@ -1024,6 +1040,8 @@ url parameters:
|
|||
|
||||
* `pw=hunter2` for password auth
|
||||
* if you enabled `--usernames` then do `pw=username:password` instead
|
||||
* `nopw` disables embedding the password (if provided) into item-URLs in the feed
|
||||
* `nopw=a` disables mentioning the password anywhere at all in the feed; may break some readers
|
||||
* `recursive` to also include subfolders
|
||||
* `title=foo` changes the feed title (default: folder name)
|
||||
* `fext=mp3,opus` only include mp3 and opus files (default: all)
|
||||
|
@ -1035,6 +1053,28 @@ url parameters:
|
|||
* uppercase = reverse-sort; `M` = oldest file first
|
||||
|
||||
|
||||
## opds feeds
|
||||
|
||||
browse and download files from your e-book reader
|
||||
|
||||
enabled with the `opds` volflag or `--opds` global option
|
||||
|
||||
add `?opds` to the end of the url you would like to browse, then input that in your opds client.
|
||||
for example: `https://copyparty.example/books/?opds`.
|
||||
|
||||
to log in with a password, enter it into either of the username or password fields in your client.
|
||||
|
||||
- if you've enabled `--usernames`, then you need to enter both username and password .
|
||||
|
||||
note: some clients (e.g. Moon+ Reader) will not send the password when downloading cover images, which will
|
||||
cause your ip to be banned by copyparty. to work around this, you can grant the [`g` permission](#accounts-and-volumes)
|
||||
to unauthenticated requests and enable [filekeys](#filekeys) to prevent guessing filenames. for example:
|
||||
`-vbooks:books:r,ed:g:c,fk,opds`
|
||||
|
||||
by default, not all file types will be listed in opds feeds. to change this, add the extension to
|
||||
`--opds-exts` (volflag: `opds_exts`), or empty the list to list everything
|
||||
|
||||
|
||||
## recent uploads
|
||||
|
||||
list all recent uploads by clicking "show recent uploads" in the controlpanel
|
||||
|
@ -1063,11 +1103,12 @@ plays almost every audio format there is (if the server has FFmpeg installed fo
|
|||
|
||||
the following audio formats are usually always playable, even without FFmpeg: `aac|flac|m4a|mp3|ogg|opus|wav`
|
||||
|
||||
some hilights:
|
||||
some highlights:
|
||||
* OS integration; control playback from your phone's lockscreen ([windows](https://user-images.githubusercontent.com/241032/233213022-298a98ba-721a-4cf1-a3d4-f62634bc53d5.png) // [iOS](https://user-images.githubusercontent.com/241032/142711926-0700be6c-3e31-47b3-9928-53722221f722.png) // [android](https://user-images.githubusercontent.com/241032/233212311-a7368590-08c7-4f9f-a1af-48ccf3f36fad.png))
|
||||
* shows the audio waveform in the seekbar
|
||||
* not perfectly gapless but can get really close (see settings + eq below); good enough to enjoy gapless albums as intended
|
||||
* videos can be played as audio, without wasting bandwidth on the video
|
||||
* adding `?v` to the end of an audio/video/image link will make it open in the mediaplayer
|
||||
|
||||
click the `play` link next to an audio file, or copy the link target to [share it](https://a.ocv.me/pub/demo/music/Ubiktune%20-%20SOUNDSHOCK%202%20-%20FM%20FUNK%20TERRROR!!/#af-1fbfba61&t=18) (optionally with a timestamp to start playing from, like that example does)
|
||||
|
||||
|
@ -1294,6 +1335,12 @@ print a qr-code [(screenshot)](https://user-images.githubusercontent.com/241032/
|
|||
* `--qrl lootbox/?pw=hunter2` appends to the url, linking to the `lootbox` folder with password `hunter2`
|
||||
* `--qrz 1` forces 1x zoom instead of autoscaling to fit the terminal size
|
||||
* 1x may render incorrectly on some terminals/fonts, but 2x should always work
|
||||
* `--qr-pin 1` makes the qr-code stick to the bottom of the console (never scrolls away)
|
||||
* `--qr-file qr.txt:1:2` writes a small qr-code to `qr.txt`
|
||||
* `--qr-file qr.txt:2:2` writes a big qr-code to `qr.txt`
|
||||
* `--qr-file qr.svg:1:2` writes a vector-graphics qr-code to `qr.svg`
|
||||
* `--qr-file qr.png:8:4:333333:ffcc55` writes an 8x-magnified yellow-on-gray `qr.png`
|
||||
* `--qr-file qr.png:8:4::ffffff` writes an 8x-magnified white-on-transparent `qr.png`
|
||||
|
||||
it uses the server hostname if [mdns](#mdns) is enabled, otherwise it'll use your external ip (default route) unless `--qri` specifies a specific ip-prefix or domain
|
||||
|
||||
|
@ -1317,6 +1364,16 @@ some recommended FTP / FTPS clients; `wark` = example password:
|
|||
* https://rclone.org/ does FTPS with `tls=false explicit_tls=true`
|
||||
* `lftp -u k,wark -p 3921 127.0.0.1 -e ls`
|
||||
* `lftp -u k,wark -p 3990 127.0.0.1 -e 'set ssl:verify-certificate no; ls'`
|
||||
* `curl ftp://127.0.0.1:3921/` (plaintext ftp)
|
||||
* `curl --ssl-reqd ftp://127.0.0.1:3990/` (encrypted ftps)
|
||||
|
||||
config file example, which restricts FTP to only use ports 3921 and 12000-12099 so all of those ports must be opened in your firewall:
|
||||
|
||||
```yaml
|
||||
[global]
|
||||
ftp: 3921
|
||||
ftp-pr: 12000-12099
|
||||
```
|
||||
|
||||
|
||||
## webdav server
|
||||
|
@ -1332,7 +1389,7 @@ general usage:
|
|||
on macos, connect from finder:
|
||||
* [Go] -> [Connect to Server...] -> http://192.168.123.1:3923/
|
||||
|
||||
in order to grant full write-access to webdav clients, the volflag `daw` must be set and the account must also have delete-access (otherwise the client won't be allowed to replace the contents of existing files, which is how webdav works)
|
||||
to upload or edit files with WebDAV clients, enable the `daw` volflag (because most WebDAV clients expect this) and give your account the delete-permission. This avoids getting several copies of the same file on the server. HOWEVER: This will also make all PUT-uploads overwrite existing files if the user has delete-access, so use with caution.
|
||||
|
||||
> note: if you have enabled [IdP authentication](#identity-providers) then that may cause issues for some/most webdav clients; see [the webdav section in the IdP docs](https://github.com/9001/copyparty/blob/hovudstraum/docs/idp.md#connecting-webdav-clients)
|
||||
|
||||
|
@ -1412,6 +1469,7 @@ and some minor issues,
|
|||
* win10 onwards does not allow connecting anonymously / without accounts
|
||||
* python3 only
|
||||
* slow (the builtin webdav support in windows is 5x faster, and rclone-webdav is 30x faster)
|
||||
* those numbers are specifically for copyparty's smb-server (because it sucks); other smb-servers should be similar to webdav
|
||||
|
||||
known client bugs:
|
||||
* on win7 only, `--smb1` is much faster than smb2 (default) because it keeps rescanning folders on smb2
|
||||
|
@ -1532,6 +1590,7 @@ the same arguments can be set as volflags, in addition to `d2d`, `d2ds`, `d2t`,
|
|||
|
||||
note:
|
||||
* upload-times can be displayed in the file listing by enabling the `.up_at` metadata key, either globally with `-e2d -mte +.up_at` or per-volume with volflags `e2d,mte=+.up_at` (will have a ~17% performance impact on directory listings)
|
||||
* and file checksums can be shown with global-option `-e2d -mte +w` or volflag `e2d,mte=+w` (always active for users with permission `a`)
|
||||
* `e2tsr` is probably always overkill, since `e2ds`/`e2dsa` would pick up any file modifications and `e2ts` would then reindex those, unless there is a new copyparty version with new parsers and the release note says otherwise
|
||||
|
||||
config file example (these options are recommended btw):
|
||||
|
@ -1615,6 +1674,7 @@ set upload rules using volflags, some examples:
|
|||
* `:c,nosub` disallow uploading into subdirectories; goes well with `rotn` and `rotf`:
|
||||
* `:c,rotn=1000,2` moves uploads into subfolders, up to 1000 files in each folder before making a new one, two levels deep (must be at least 1)
|
||||
* `:c,rotf=%Y/%m/%d/%H` enforces files to be uploaded into a structure of subfolders according to that date format
|
||||
* `:c,rotf_tz=Europe/Oslo` sets the timezone (default is UTC unless global-option `rotf-tz` is changed)
|
||||
* if someone uploads to `/foo/bar` the path would be rewritten to `/foo/bar/2021/08/06/23` for example
|
||||
* but the actual value is not verified, just the structure, so the uploader can choose any values which conform to the format string
|
||||
* just to avoid additional complexity in up2k which is enough of a mess already
|
||||
|
@ -1920,6 +1980,10 @@ you can disable the built-in password-based login system, and instead replace it
|
|||
|
||||
* the regular config-defined users will be used as a fallback for requests which don't include a valid (trusted) IdP username header
|
||||
|
||||
* `--auth-ord` configured auth precedence, for example to allow overriding the IdP with a copyparty password
|
||||
|
||||
* the login/logout links/buttons can be replaced with links to your IdP with `--idp-login` and `--idp-logout` , for example `--idp-login /idp/login/?redir={dst}` will expand `{dst}` to the page the user was on when clicking Login
|
||||
|
||||
* if your IdP-server is slow, consider `--idp-cookie` and let requests with the cookie `cppws` bypass the IdP; experimental sessions-based feature added for a party
|
||||
|
||||
some popular identity providers are [Authelia](https://www.authelia.com/) (config-file based) and [authentik](https://goauthentik.io/) (GUI-based, more complex)
|
||||
|
@ -2160,7 +2224,7 @@ when connecting the reverse-proxy to `127.0.0.1` instead (the basic and/or old-f
|
|||
|
||||
in summary, `haproxy > caddy > traefik > nginx > apache > lighttpd`, and use uds when possible (traefik does not support it yet)
|
||||
|
||||
* if these results are bullshit because my config exampels are bad, please submit corrections!
|
||||
* if these results are bullshit because my config examples are bad, please submit corrections!
|
||||
|
||||
|
||||
## permanent cloudflare tunnel
|
||||
|
@ -2296,6 +2360,7 @@ buggy feature? rip it out by setting any of the following environment variables
|
|||
| `PRTY_NO_SQLITE` | disable all database-related functionality (file indexing, metadata indexing, most file deduplication logic) |
|
||||
| `PRTY_NO_TLS` | disable native HTTPS support; if you still want to accept HTTPS connections then TLS must now be terminated by a reverse-proxy |
|
||||
| `PRTY_NO_TPOKE` | disable systemd-tmpfilesd avoider |
|
||||
| `PRTY_UNSAFE_STATE` | allow storing secrets into emergency-fallback locations |
|
||||
|
||||
example: `PRTY_NO_IFADDR=1 python3 copyparty-sfx.py`
|
||||
|
||||
|
@ -2331,6 +2396,15 @@ after installing, start either the system service or the user service and naviga
|
|||
does not exist yet; there are rumours that it is being packaged! keep an eye on this space...
|
||||
|
||||
|
||||
## homebrew formulae
|
||||
|
||||
`brew install copyparty ffmpeg` -- https://formulae.brew.sh/formula/copyparty
|
||||
|
||||
should work on all macs (both intel and apple silicon) and all relevant macos versions
|
||||
|
||||
the homebrew package is maintained by the homebrew team (thanks!)
|
||||
|
||||
|
||||
## nix package
|
||||
|
||||
`nix profile install github:9001/copyparty`
|
||||
|
@ -2344,7 +2418,7 @@ some recommended dependencies are enabled by default; [override the package](htt
|
|||
|
||||
## nixos module
|
||||
|
||||
for this setup, you will need a [flake-enabled](https://nixos.wiki/wiki/Flakes) installation of NixOS.
|
||||
for [flake-enabled](https://nixos.wiki/wiki/Flakes) installations of NixOS:
|
||||
|
||||
```nix
|
||||
{
|
||||
|
@ -2371,10 +2445,41 @@ for this setup, you will need a [flake-enabled](https://nixos.wiki/wiki/Flakes)
|
|||
}
|
||||
```
|
||||
|
||||
if you don't use a flake in your configuration, you can use other dependency management tools like [npins](https://github.com/andir/npins), [niv](https://github.com/nmattia/niv), or even plain [`fetchTarball`](https://nix.dev/manual/nix/stable/language/builtins#builtins-fetchTarball), like so:
|
||||
|
||||
```nix
|
||||
{ pkgs, ... }:
|
||||
|
||||
let
|
||||
# npins example, adjust for your setup. copyparty should be a path to the downloaded repo
|
||||
# for niv, just replace the npins folder import with the sources.nix file
|
||||
copyparty = (import ./npins).copyparty;
|
||||
|
||||
# or with fetchTarball:
|
||||
copyparty = fetchTarball "https://github.com/9001/copyparty/archive/hovudstraum.tar.gz";
|
||||
in
|
||||
|
||||
{
|
||||
# load the copyparty NixOS module
|
||||
imports = [ "${copyparty}/contrib/nixos/modules/copyparty.nix" ];
|
||||
|
||||
# add the copyparty overlay to expose the package to the module
|
||||
nixpkgs.overlays = [ (import "${copyparty}/contrib/package/nix/overlay.nix") ];
|
||||
# (optional) install the package globally
|
||||
environment.systemPackages = [ pkgs.copyparty ];
|
||||
# configure the copyparty module
|
||||
services.copyparty.enable = true;
|
||||
}
|
||||
```
|
||||
|
||||
copyparty on NixOS is configured via `services.copyparty` options, for example:
|
||||
```nix
|
||||
services.copyparty = {
|
||||
enable = true;
|
||||
# the user to run the service as
|
||||
user = "copyparty";
|
||||
# the group to run the service as
|
||||
group = "copyparty";
|
||||
# directly maps to values in the [global] section of the copyparty config.
|
||||
# see `copyparty --help` for available options
|
||||
settings = {
|
||||
|
@ -2399,6 +2504,12 @@ services.copyparty = {
|
|||
k.passwordFile = "/run/keys/copyparty/k_password";
|
||||
};
|
||||
|
||||
# create a group
|
||||
groups = {
|
||||
# users "ed" and "k" are part of the group g1
|
||||
g1 = [ "ed" "k" ];
|
||||
};
|
||||
|
||||
# create a volume
|
||||
volumes = {
|
||||
# create a volume at "/" (the webroot), which will
|
||||
|
@ -2557,11 +2668,20 @@ sync folders to/from copyparty
|
|||
|
||||
NOTE: full bidirectional sync, like what [nextcloud](https://docs.nextcloud.com/server/latest/user_manual/sv/files/desktop_mobile_sync.html) and [syncthing](https://syncthing.net/) does, will never be supported! Only single-direction sync (server-to-client, or client-to-server) is possible with copyparty
|
||||
|
||||
* if you want bidirectional sync, then copyparty and syncthing *should* be entirely safe to combine; they should be able to collaborate on the same folders without causing any trouble for eachother. Many people do this, and there have been no issues so far. But, if you *do* encounter any problems, please [file a copyparty bug](https://github.com/9001/copyparty/issues/new/choose) and I'll try to help -- just keep in mind I've never used syncthing before :-)
|
||||
|
||||
the commandline uploader [u2c.py](https://github.com/9001/copyparty/tree/hovudstraum/bin#u2cpy) with `--dr` is the best way to sync a folder to copyparty; verifies checksums and does files in parallel, and deletes unexpected files on the server after upload has finished which makes file-renames really cheap (it'll rename serverside and skip uploading)
|
||||
|
||||
if you want to sync with `u2c.py` then:
|
||||
* the `e2dsa` option (either globally or volflag) must be enabled on the server for the volumes you're syncing into
|
||||
* ...but DON'T enable global-options `no-hash` or `no-idx` (or volflags `nohash` / `noidx`), or at least make sure they are configured so they do not affect anything you are syncing into
|
||||
* ...and u2c needs the delete-permission, so either `rwd` at minimum, or just `A` which is the same as `rwmd.a`
|
||||
* quick reminder that `a` and `A` are different permissions, and `.` is very useful for sync
|
||||
|
||||
alternatively there is [rclone](./docs/rclone.md) which allows for bidirectional sync and is *way* more flexible (stream files straight from sftp/s3/gcs to copyparty, ...), although there is no integrity check and it won't work with files over 100 MiB if copyparty is behind cloudflare
|
||||
|
||||
* starting from rclone v1.63, rclone is faster than u2c.py on low-latency connections
|
||||
* but this is only true for the initial upload; u2c will be faster for periodic syncing
|
||||
|
||||
|
||||
## mount as drive
|
||||
|
@ -2603,6 +2723,8 @@ there is no iPhone app, but the following shortcuts are almost as good:
|
|||
* can download links and rehost the target file on copyparty (see first comment inside the shortcut)
|
||||
* pics become lowres if you share from gallery to shortcut, so better to launch the shortcut and pick stuff from there
|
||||
|
||||
if you want to run the copyparty server on your iPhone or iPad, see [install on iOS](#install-on-iOS)
|
||||
|
||||
|
||||
# performance
|
||||
|
||||
|
@ -2631,6 +2753,10 @@ below are some tweaks roughly ordered by usefulness:
|
|||
* using [pypy](https://www.pypy.org/) instead of [cpython](https://www.python.org/) *can* be 70% faster for some workloads, but slower for many others
|
||||
* and pypy can sometimes crash on startup with `-j0` (TODO make issue)
|
||||
|
||||
* if you are running the copyparty server **on Windows or Macos:**
|
||||
* `--casechk=n` makes it much faster, but also awakens [the usual surprises](https://github.com/9001/copyparty/issues/781) you expect from a case-insensitive filesystem
|
||||
* this is the same as `casechk: n` in a config-file
|
||||
|
||||
|
||||
## client-side
|
||||
|
||||
|
@ -2873,6 +2999,20 @@ example: `PRTY_NO_PIL=1 python3 copyparty-sfx.py`
|
|||
* python2.7 on windows: `PRTY_NO_FFMPEG` + `PRTY_NO_FFPROBE` saves startup time
|
||||
|
||||
|
||||
### dependency unvendoring
|
||||
|
||||
force use of system modules instead of the vendored versions:
|
||||
|
||||
| env-var | what it does |
|
||||
| -------------------- | ------------ |
|
||||
| `PRTY_SYS_ALL` | all of the below |
|
||||
| `PRTY_SYS_DNSLIB` | replace [stolen/dnslib](./copyparty/stolen/dnslib) with [upstream](https://pypi.org/project/dnslib/) |
|
||||
| `PRTY_SYS_IFADDR` | replace [stolen/ifaddr](./copyparty/stolen/ifaddr) with [upstream](https://pypi.org/project/ifaddr/) |
|
||||
| `PRTY_SYS_QRCG` | replace [stolen/qrcodegen.py](./copyparty/stolen/qrcodegen.py) with [upstream](https://github.com/nayuki/QR-Code-generator/blob/master/python/qrcodegen.py) |
|
||||
|
||||
to debug, run copyparty with `PRTY_MODSPEC=1` to see where it's getting each module from
|
||||
|
||||
|
||||
## optional gpl stuff
|
||||
|
||||
some bundled tools have copyleft dependencies, see [./bin/#mtag](bin/#mtag)
|
||||
|
@ -2935,6 +3075,29 @@ if you want thumbnails (photos+videos) and you're okay with spending another 132
|
|||
|
||||
* or if you want to use `vips` for photo-thumbs instead, `pkg install libvips && python -m pip install --user -U wheel && python -m pip install --user -U pyvips && (cd /data/data/com.termux/files/usr/lib/; ln -s libgobject-2.0.so{,.0}; ln -s libvips.so{,.42})`
|
||||
|
||||
if you are suddenly unable to access storage (permission issues), try forcequitting termux, revoke all of its permissions in android settings, and run the command `termux-setup-storage`
|
||||
|
||||
|
||||
# install on iOS
|
||||
|
||||
first install one of the following:
|
||||
* [a-Shell mini](https://apps.apple.com/us/app/a-shell-mini/id1543537943) gives you the essential features
|
||||
* [a-Shell](https://apps.apple.com/us/app/a-shell/id1473805438) also enables audio transcoding and better thubmnails
|
||||
|
||||
and then copypaste the following command into `a-Shell`:
|
||||
|
||||
```sh
|
||||
curl -L https://github.com/9001/copyparty/raw/refs/heads/hovudstraum/contrib/setup-ashell.sh | sh
|
||||
```
|
||||
|
||||
what this does:
|
||||
* creates a basic [config file](#accounts-and-volumes) named `cpc` which you can edit with `vim cpc`
|
||||
* adds the command `cpp` to launch copyparty with that config file
|
||||
|
||||
known issues:
|
||||
* cannot run in the background; it needs to be on-screen to accept connections / uploads / downloads
|
||||
* the best way to exit copyparty is to swipe away the app
|
||||
|
||||
|
||||
# reporting bugs
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# [`u2c.py`](u2c.py)
|
||||
* command-line up2k client [(webm)](https://ocv.me/stuff/u2cli.webm)
|
||||
* file uploads, file-search, autoresume of aborted/broken uploads
|
||||
* sync local folder to server
|
||||
* [sync local folder to server](https://github.com/9001/copyparty/#folder-sync)
|
||||
* generally faster than browsers
|
||||
* if something breaks just restart it
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#!/bin/sh
|
||||
#!/bin/bash
|
||||
# usage: ./bubbleparty.sh ./copyparty-sfx.py ....
|
||||
bwrap \
|
||||
--unshare-all \
|
||||
|
@ -9,7 +9,7 @@ bwrap \
|
|||
--dev-bind /dev /dev \
|
||||
--dir /tmp \
|
||||
--dir /var \
|
||||
--bind $(pwd) $(pwd) \
|
||||
--bind "$(pwd)" "$(pwd)" \
|
||||
--share-net \
|
||||
--die-with-parent \
|
||||
--file 11 /etc/passwd \
|
||||
|
|
|
@ -8,7 +8,7 @@ import sqlite3
|
|||
import argparse
|
||||
|
||||
DB_VER1 = 3
|
||||
DB_VER2 = 5
|
||||
DB_VER2 = 6
|
||||
|
||||
BY_PATH = None
|
||||
NC = None
|
||||
|
@ -39,7 +39,7 @@ def ls(db):
|
|||
print(f"{nfiles} files")
|
||||
print(f"{ntags} tags\n")
|
||||
|
||||
print("number of occurences for each tag,")
|
||||
print("number of occurrences for each tag,")
|
||||
print(" 'x' = file has no tags")
|
||||
print(" 't:mtp' = the mtp flag (file not mtp processed yet)")
|
||||
print()
|
||||
|
|
|
@ -46,7 +46,7 @@ def main(cli, vn, rem):
|
|||
|
||||
# uncomment one of these:
|
||||
send_http_302_temporary_redirect(cli, new_path)
|
||||
#send_http_301_permanent_redirect(cli, new_path)
|
||||
#send_errorpage_with_redirect_link(cli, new_path)
|
||||
# send_http_301_permanent_redirect(cli, new_path)
|
||||
# send_errorpage_with_redirect_link(cli, new_path)
|
||||
|
||||
return "true"
|
||||
|
|
|
@ -28,10 +28,19 @@ these are `--xiu` hooks; unlike `xbu` and `xau` (which get executed on every sin
|
|||
* [reject-extension.py](reject-extension.py) rejects uploads if they match a list of file extensions
|
||||
* [reloc-by-ext.py](reloc-by-ext.py) redirects an upload to another destination based on the file extension
|
||||
* good example of the `reloc` [hook effect](https://github.com/9001/copyparty/blob/hovudstraum/docs/devnotes.md#hook-effects)
|
||||
* [reject-and-explain.py](reject-and-explain.py) shows a custom error-message when it rejects an upload
|
||||
* [reject-ramdisk.py](reject-ramdisk.py) rejects the upload if the destination is a ramdisk
|
||||
* this hook uses the `I` flag which makes it 140x faster, but if the plugin has a bug it may crash copyparty
|
||||
|
||||
|
||||
# on message
|
||||
* [wget.py](wget.py) lets you download files by POSTing URLs to copyparty
|
||||
* [wget-i.py](wget-i.py) is an import-safe modification of this hook (starts 140x faster, but higher chance of bugs)
|
||||
* [qbittorrent-magnet.py](qbittorrent-magnet.py) starts downloading a torrent if you post a magnet url
|
||||
* [usb-eject.py](usb-eject.py) adds web-UI buttons to safe-remove usb flashdrives shared through copyparty
|
||||
* [msg-log.py](msg-log.py) is a guestbook; logs messages to a doc in the same folder
|
||||
|
||||
|
||||
# general concept demos
|
||||
* [import-me.py](import-me.py) shows how the `I` flag makes the hook 140x faster (but you need to be Very Careful when writing the plugin)
|
||||
* [wget-i.py](wget-i.py) is an import-safe modification of [wget.py](wget.py)
|
||||
|
|
55
bin/hooks/import-me.py
Normal file
55
bin/hooks/import-me.py
Normal file
|
@ -0,0 +1,55 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
from typing import Any
|
||||
|
||||
_ = r"""
|
||||
the fastest hook in the west
|
||||
(runs directly inside copyparty, not as a subprocess)
|
||||
|
||||
example usage as global config:
|
||||
--xbu I,bin/hooks/import-me.py
|
||||
|
||||
example usage as a volflag (per-volume config):
|
||||
-v srv/inc:inc:r:rw,ed:c,xbu=I,bin/hooks/import-me.py
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
(share filesystem-path srv/inc as volume /inc,
|
||||
readable by everyone, read-write for user 'ed',
|
||||
running this plugin on all uploads with the params listed below)
|
||||
|
||||
example usage as a volflag in a copyparty config file:
|
||||
[/inc]
|
||||
srv/inc
|
||||
accs:
|
||||
r: *
|
||||
rw: ed
|
||||
flags:
|
||||
xbu: I,bin/hooks/import-me.py
|
||||
|
||||
parameters explained,
|
||||
I = import; do not fork / subprocess
|
||||
|
||||
IMPORTANT NOTE:
|
||||
because this hook is running inside copyparty, you need to
|
||||
be EXCEPTIONALLY CAREFUL to avoid side-effects, for example
|
||||
DO NOT os.chdir() or anything like that, and also make sure
|
||||
that the name of this file is unique (cannot be the same as
|
||||
an existing python module/library)
|
||||
"""
|
||||
|
||||
|
||||
def main(ka: dict[str, Any]) -> dict[str, Any]:
|
||||
# "ka" is a dictionary with info from copyparty...
|
||||
|
||||
# but because we are running inside copyparty, we don't need such courtesies;
|
||||
import inspect
|
||||
|
||||
cf = inspect.currentframe().f_back.f_back.f_back
|
||||
t = "hello from hook; I am able to peek into copyparty's memory like so:\n function name: %s\n variables:\n %s\n"
|
||||
t2 = "\n ".join([("%r: %r" % (k, v))[:99] for k, v in cf.f_locals.items()][:9])
|
||||
logger = ka["log"]
|
||||
logger(t % (cf.f_code, t2))
|
||||
|
||||
# must return a dictionary with:
|
||||
# "rc": the retcode; 0 is ok
|
||||
return {"rc": 0}
|
|
@ -9,7 +9,7 @@ from plyer import notification
|
|||
_ = r"""
|
||||
show os notification on upload; works on windows, linux, macos, android
|
||||
|
||||
depdencies:
|
||||
dependencies:
|
||||
windows: python3 -m pip install --user -U plyer
|
||||
linux: python3 -m pip install --user -U plyer
|
||||
macos: python3 -m pip install --user -U plyer pyobjus
|
||||
|
|
60
bin/hooks/reject-and-explain.py
Normal file
60
bin/hooks/reject-and-explain.py
Normal file
|
@ -0,0 +1,60 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
_ = r"""
|
||||
reject file upload (with a nice explanation why)
|
||||
|
||||
example usage as global config:
|
||||
--xbu j,c1,bin/hooks/reject-and-explain.py
|
||||
|
||||
example usage as a volflag (per-volume config):
|
||||
-v srv/inc:inc:r:rw,ed:c,xbu=j,c1,bin/hooks/reject-and-explain.py
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
(share filesystem-path srv/inc as volume /inc,
|
||||
readable by everyone, read-write for user 'ed',
|
||||
running this plugin on all uploads with the params listed below)
|
||||
|
||||
example usage as a volflag in a copyparty config file:
|
||||
[/inc]
|
||||
srv/inc
|
||||
accs:
|
||||
r: *
|
||||
rw: ed
|
||||
flags:
|
||||
xbu: j,c1,bin/hooks/reject-and-explain.py
|
||||
|
||||
parameters explained,
|
||||
xbu = execute-before-upload (can also be xau, execute-after-upload)
|
||||
j = this hook needs upload information as json (not just the filename)
|
||||
c1 = this hook returns json on stdout, so tell copyparty to read that
|
||||
"""
|
||||
|
||||
|
||||
def main():
|
||||
inf = json.loads(sys.argv[1])
|
||||
vdir, fn = os.path.split(inf["vp"])
|
||||
print("inf[vp] = %r" % (inf["vp"],), file=sys.stderr)
|
||||
|
||||
# the following is what decides if we'll accept the upload or reject it:
|
||||
# we check if the upload-folder url matches the following regex-pattern:
|
||||
ok = re.search(r"(^|/)day[0-9]+$", vdir, re.IGNORECASE)
|
||||
|
||||
if ok:
|
||||
# allow the upload
|
||||
print("{}")
|
||||
return
|
||||
|
||||
# the upload was rejected; display the following errortext:
|
||||
errmsg = "Files can only be uploaded into a folder named 'DayN' where N is a number, for example 'Day573'. This file was REJECTED: "
|
||||
errmsg += inf["vp"] # if you want to mention the file's url
|
||||
print(json.dumps({"rejectmsg": errmsg}))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
72
bin/hooks/reject-ramdisk.py
Normal file
72
bin/hooks/reject-ramdisk.py
Normal file
|
@ -0,0 +1,72 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import threading
|
||||
from argparse import Namespace
|
||||
|
||||
from jinja2.nodes import Name
|
||||
from copyparty.fsutil import Fstab
|
||||
from typing import Any, Optional
|
||||
|
||||
|
||||
_ = r"""
|
||||
reject an upload if the target folder is on a ramdisk; useful when you
|
||||
have a volume where some folders inside are ramdisks but others aren't
|
||||
|
||||
example usage as global config:
|
||||
--xbu I,bin/hooks/reject-ramdisk.py
|
||||
|
||||
example usage as a volflag (per-volume config):
|
||||
-v srv/inc:inc:r:rw,ed:c,xbu=I,bin/hooks/reject-ramdisk.py
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
(share filesystem-path srv/inc as volume /inc,
|
||||
readable by everyone, read-write for user 'ed',
|
||||
running this plugin on all uploads with the params listed below)
|
||||
|
||||
example usage as a volflag in a copyparty config file:
|
||||
[/inc]
|
||||
srv/inc
|
||||
accs:
|
||||
r: *
|
||||
rw: ed
|
||||
flags:
|
||||
xbu: I,bin/hooks/reject-ramdisk.py
|
||||
|
||||
parameters explained,
|
||||
I = import; do not fork / subprocess
|
||||
|
||||
IMPORTANT NOTE:
|
||||
because this hook is imported inside copyparty, you need to
|
||||
be EXCEPTIONALLY CAREFUL to avoid side-effects, for example
|
||||
DO NOT os.chdir() or anything like that, and also make sure
|
||||
that the name of this file is unique (cannot be the same as
|
||||
an existing python module/library)
|
||||
"""
|
||||
|
||||
|
||||
mutex = threading.Lock()
|
||||
fstab: Optional[Fstab] = None
|
||||
|
||||
|
||||
def main(ka: dict[str, Any]) -> dict[str, Any]:
|
||||
global fstab
|
||||
with mutex:
|
||||
log = ka["log"] # this is a copyparty NamedLogger function
|
||||
if not fstab:
|
||||
log("<HOOK:RAMDISK> creating fstab", 6)
|
||||
args = Namespace()
|
||||
args.mtab_age = 1 # cache the filesystem info for 1 sec
|
||||
fstab = Fstab(log, args, False)
|
||||
|
||||
ap = ka["ap"] # abspath the upload is going to
|
||||
fs, mp = fstab.get(ap) # figure out what the filesystem is
|
||||
ramdisk = fs in ("tmpfs", "overlay") # looks like a ramdisk?
|
||||
|
||||
# log("<HOOK:RAMDISK> fs=%r" % (fs,))
|
||||
|
||||
if ramdisk:
|
||||
t = "Upload REJECTED because destination is a ramdisk"
|
||||
return {"rc": 1, "rejectmsg": t}
|
||||
|
||||
return {"rc": 0}
|
97
bin/hooks/wget-i.py
Executable file
97
bin/hooks/wget-i.py
Executable file
|
@ -0,0 +1,97 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import threading
|
||||
import subprocess as sp
|
||||
|
||||
|
||||
_ = r"""
|
||||
use copyparty as a file downloader by POSTing URLs as
|
||||
application/x-www-form-urlencoded (for example using the
|
||||
📟 message-to-server-log in the web-ui)
|
||||
|
||||
this hook is a modified copy of wget.py, modified to
|
||||
make it import-safe so it can be run with the 'I' flag,
|
||||
which speeds up the startup time of the hook by 140x
|
||||
|
||||
example usage as global config:
|
||||
--xm aw,I,bin/hooks/wget-i.py
|
||||
|
||||
parameters explained,
|
||||
xm = execute on message-to-server-log
|
||||
aw = only users with write-access can use this
|
||||
I = import; do not fork / subprocess
|
||||
|
||||
example usage as a volflag (per-volume config):
|
||||
-v srv/inc:inc:r:rw,ed:c,xm=aw,I,bin/hooks/wget.py
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
(share filesystem-path srv/inc as volume /inc,
|
||||
readable by everyone, read-write for user 'ed',
|
||||
running this plugin on all messages with the params explained above)
|
||||
|
||||
example usage as a volflag in a copyparty config file:
|
||||
[/inc]
|
||||
srv/inc
|
||||
accs:
|
||||
r: *
|
||||
rw: ed
|
||||
flags:
|
||||
xm: aw,I,bin/hooks/wget.py
|
||||
|
||||
the volflag examples only kicks in if you send the message
|
||||
while you're in the /inc folder (or any folder below there)
|
||||
|
||||
IMPORTANT NOTE:
|
||||
because this hook uses the 'I' flag to run inside copyparty,
|
||||
many other flags will not work (f,j,c3,t3600 as seen in the
|
||||
original wget.py), and furthermore + more importantly we
|
||||
need to be EXCEPTIONALLY CAREFUL to avoid side-effects, so
|
||||
the os.chdir has been replaced with cwd=dirpath for example
|
||||
"""
|
||||
|
||||
|
||||
def do_stuff(inf):
|
||||
"""
|
||||
worker function which is executed in another thread to
|
||||
avoid blocking copyparty while the download is running,
|
||||
since we cannot use the 'f,t3600' hook-flags with 'I'
|
||||
"""
|
||||
|
||||
# first things first; grab the logger-function which copyparty is letting us borrow
|
||||
log = inf["log"]
|
||||
|
||||
url = inf["txt"]
|
||||
if "://" not in url:
|
||||
url = "https://" + url
|
||||
|
||||
proto = url.split("://")[0].lower()
|
||||
if proto not in ("http", "https", "ftp", "ftps"):
|
||||
raise Exception("bad proto {}".format(proto))
|
||||
|
||||
dirpath = inf["ap"]
|
||||
|
||||
name = url.split("?")[0].split("/")[-1]
|
||||
msg = "-- DOWNLOADING " + name
|
||||
log(msg)
|
||||
tfn = os.path.join(dirpath, msg)
|
||||
open(tfn, "wb").close()
|
||||
|
||||
cmd = ["wget", "--trust-server-names", "-nv", "--", url]
|
||||
|
||||
try:
|
||||
# two things to note here:
|
||||
# - cannot use the `c3` hook-flag with `I` so mute output with stdout=sp.DEVNULL instead;
|
||||
# - MUST NOT use os.chdir with 'I' so use cwd=dirpath instead
|
||||
sp.check_call(cmd, cwd=dirpath, stdout=sp.DEVNULL)
|
||||
except:
|
||||
t = "-- FAILED TO DOWNLOAD " + name
|
||||
log(t, 3) # 3=yellow=warning
|
||||
open(os.path.join(dirpath, t), "wb").close()
|
||||
raise # have copyparty scream about the details in the log
|
||||
|
||||
os.unlink(tfn)
|
||||
|
||||
|
||||
def main(inf):
|
||||
threading.Thread(target=do_stuff, args=(inf,), daemon=True).start()
|
|
@ -66,7 +66,7 @@ def main():
|
|||
try:
|
||||
sp.check_call(cmd)
|
||||
except:
|
||||
t = "-- FAILED TO DONWLOAD " + name
|
||||
t = "-- FAILED TO DOWNLOAD " + name
|
||||
print(f"{t}\n", end="")
|
||||
open(t, "wb").close()
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ example copyparty config to use this:
|
|||
--urlform save,get -vsrv/hello:hello:w:c,e2ts,mtp=guestbook=t10,ad,p,bin/mtag/guestbook-read.py:mte=+guestbook
|
||||
|
||||
explained:
|
||||
for realpath srv/hello (served at /hello), write-only for eveyrone,
|
||||
for realpath srv/hello (served at /hello), write-only for everyone,
|
||||
enable file analysis on upload (e2ts),
|
||||
use mtp plugin "bin/mtag/guestbook-read.py" to provide metadata tag "guestbook",
|
||||
do this on all uploads regardless of extension,
|
||||
|
|
|
@ -11,7 +11,7 @@ example copyparty config to use this:
|
|||
--urlform save,get -vsrv/hello:hello:w:c,e2ts,mtp=xgb=ebin,t10,ad,p,bin/mtag/guestbook.py:mte=+xgb
|
||||
|
||||
explained:
|
||||
for realpath srv/hello (served at /hello),write-only for eveyrone,
|
||||
for realpath srv/hello (served at /hello),write-only for everyone,
|
||||
enable file analysis on upload (e2ts),
|
||||
use mtp plugin "bin/mtag/guestbook.py" to provide metadata tag "xgb",
|
||||
do this on all uploads with the file extension "bin",
|
||||
|
|
|
@ -84,7 +84,7 @@ def main():
|
|||
# on success, delete the .bin file which contains the URL
|
||||
os.unlink(fp)
|
||||
except:
|
||||
open("-- FAILED TO DONWLOAD " + name, "wb").close()
|
||||
open("-- FAILED TO DOWNLOAD " + name, "wb").close()
|
||||
|
||||
os.unlink(tfn)
|
||||
print(url)
|
||||
|
|
|
@ -6,8 +6,8 @@ __copyright__ = 2019
|
|||
__license__ = "MIT"
|
||||
__url__ = "https://github.com/9001/copyparty/"
|
||||
|
||||
S_VERSION = "2.0"
|
||||
S_BUILD_DT = "2024-10-01"
|
||||
S_VERSION = "2.1"
|
||||
S_BUILD_DT = "2025-09-06"
|
||||
|
||||
"""
|
||||
mount a copyparty server (local or remote) as a filesystem
|
||||
|
@ -99,7 +99,7 @@ except:
|
|||
elif MACOS:
|
||||
libfuse = "install https://osxfuse.github.io/"
|
||||
else:
|
||||
libfuse = "apt install libfuse3-3\n modprobe fuse"
|
||||
libfuse = "apt install libfuse2\n modprobe fuse"
|
||||
|
||||
m = """\033[33m
|
||||
could not import fuse; these may help:
|
||||
|
@ -359,7 +359,7 @@ class Gateway(object):
|
|||
def sendreq(self, meth, path, headers, **kwargs):
|
||||
tid = get_tid()
|
||||
if self.password:
|
||||
headers["Cookie"] = "=".join(["cppwd", self.password])
|
||||
headers["PW"] = self.password
|
||||
|
||||
try:
|
||||
c = self.getconn(tid)
|
||||
|
@ -902,9 +902,7 @@ class CPPF(Operations):
|
|||
return ret
|
||||
|
||||
def _readdir(self, path, fh=None):
|
||||
path = path.strip("/")
|
||||
dbg("readdir %r [%s]", path, fh)
|
||||
|
||||
dbg("dircache miss")
|
||||
ret = self.gw.listdir(path)
|
||||
if not self.n_dircache:
|
||||
return ret
|
||||
|
@ -914,11 +912,17 @@ class CPPF(Operations):
|
|||
self.dircache.append(cn)
|
||||
self.clean_dircache()
|
||||
|
||||
# import pprint; pprint.pprint(ret)
|
||||
return ret
|
||||
|
||||
def readdir(self, path, fh=None):
|
||||
return [".", ".."] + list(self._readdir(path, fh))
|
||||
dbg("readdir %r [%s]", path, fh)
|
||||
path = path.strip("/")
|
||||
cn = self.get_cached_dir(path)
|
||||
if cn:
|
||||
ret = cn.data
|
||||
else:
|
||||
ret = self._readdir(path, fh)
|
||||
return [".", ".."] + list(ret)
|
||||
|
||||
def read(self, path, length, offset, fh=None):
|
||||
req_max = 1024 * 1024 * 8
|
||||
|
@ -993,7 +997,6 @@ class CPPF(Operations):
|
|||
if cn:
|
||||
dents = cn.data
|
||||
else:
|
||||
dbg("cache miss")
|
||||
dents = self._readdir(dirpath)
|
||||
|
||||
try:
|
||||
|
@ -1141,10 +1144,15 @@ def main():
|
|||
if WINDOWS:
|
||||
examples.append("http://192.168.1.69:3923/music/ M:")
|
||||
|
||||
epi = "example:" + ex_pre + ex_pre.join(examples)
|
||||
epi += """\n
|
||||
NOTE: if server has --usernames enabled, then password is "username:password"
|
||||
"""
|
||||
|
||||
ap = argparse.ArgumentParser(
|
||||
formatter_class=TheArgparseFormatter,
|
||||
description="mount a copyparty server as a local filesystem -- " + ver,
|
||||
epilog="example:" + ex_pre + ex_pre.join(examples),
|
||||
epilog=epi,
|
||||
)
|
||||
# fmt: off
|
||||
ap.add_argument("base_url", type=str, help="remote copyparty URL to mount")
|
||||
|
|
|
@ -141,7 +141,7 @@ chmod 777 "$jail/tmp"
|
|||
|
||||
|
||||
# create a dev
|
||||
(cd $jail; mkdir -p dev; cd dev
|
||||
(cd "$jail"; mkdir -p dev; cd dev
|
||||
[ -e null ] || mknod -m 666 null c 1 3
|
||||
[ -e zero ] || mknod -m 666 zero c 1 5
|
||||
[ -e random ] || mknod -m 444 random c 1 8
|
||||
|
|
18
bin/u2c.py
18
bin/u2c.py
|
@ -1,8 +1,8 @@
|
|||
#!/usr/bin/env python3
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
S_VERSION = "2.11"
|
||||
S_BUILD_DT = "2025-05-18"
|
||||
S_VERSION = "2.13"
|
||||
S_BUILD_DT = "2025-09-05"
|
||||
|
||||
"""
|
||||
u2c.py: upload to copyparty
|
||||
|
@ -10,7 +10,7 @@ u2c.py: upload to copyparty
|
|||
https://github.com/9001/copyparty/blob/hovudstraum/bin/u2c.py
|
||||
|
||||
- dependencies: no
|
||||
- supports python 2.6, 2.7, and 3.3 through 3.12
|
||||
- supports python 2.6, 2.7, and 3.3 through 3.14
|
||||
- if something breaks just try again and it'll autoresume
|
||||
"""
|
||||
|
||||
|
@ -590,9 +590,10 @@ def undns(url):
|
|||
|
||||
def _scd(err, top):
|
||||
"""non-recursive listing of directory contents, along with stat() info"""
|
||||
top_ = os.path.join(top, b"")
|
||||
with os.scandir(top) as dh:
|
||||
for fh in dh:
|
||||
abspath = os.path.join(top, fh.name)
|
||||
abspath = top_ + fh.name
|
||||
try:
|
||||
yield [abspath, fh.stat()]
|
||||
except Exception as ex:
|
||||
|
@ -601,8 +602,9 @@ def _scd(err, top):
|
|||
|
||||
def _lsd(err, top):
|
||||
"""non-recursive listing of directory contents, along with stat() info"""
|
||||
top_ = os.path.join(top, b"")
|
||||
for name in os.listdir(top):
|
||||
abspath = os.path.join(top, name)
|
||||
abspath = top_ + name
|
||||
try:
|
||||
yield [abspath, os.stat(abspath)]
|
||||
except Exception as ex:
|
||||
|
@ -677,7 +679,7 @@ def walkdirs(err, tops, excl):
|
|||
yield stop, ap[len(stop) :].lstrip(sep), inf
|
||||
else:
|
||||
d, n = top.rsplit(sep, 1)
|
||||
yield d, n, os.stat(top)
|
||||
yield d or b"/", n, os.stat(top)
|
||||
|
||||
|
||||
# mostly from copyparty/util.py
|
||||
|
@ -1527,10 +1529,10 @@ def main():
|
|||
|
||||
# fmt: off
|
||||
ap = app = argparse.ArgumentParser(formatter_class=APF, description="copyparty up2k uploader / filesearch tool " + ver, epilog="""
|
||||
NOTE:
|
||||
source file/folder selection uses rsync syntax, meaning that:
|
||||
NOTE: source file/folder selection uses rsync syntax, meaning that:
|
||||
"foo" uploads the entire folder to URL/foo/
|
||||
"foo/" uploads the CONTENTS of the folder into URL/
|
||||
NOTE: if server has --usernames enabled, then password is "username:password"
|
||||
""")
|
||||
|
||||
ap.add_argument("url", type=unicode, help="server url, including destination folder")
|
||||
|
|
|
@ -7,6 +7,12 @@
|
|||
* works on windows, linux and macos
|
||||
* assumes `copyparty-sfx.py` was renamed to `copyparty.py` in the same folder as `copyparty.bat`
|
||||
|
||||
### [`setup-ashell.sh`](setup-ashell.sh)
|
||||
* run copyparty on an iPhone/iPad using [a-Shell](https://holzschu.github.io/a-Shell_iOS/)
|
||||
* not very useful due to limitations in iOS:
|
||||
* not able to share all of your phone's storage
|
||||
* cannot run in the background
|
||||
|
||||
### [`index.html`](index.html)
|
||||
* drop-in redirect from an httpd to copyparty
|
||||
* assumes the webserver and copyparty is running on the same server/IP
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
# generate the list of permitted IP ranges like so:
|
||||
# (curl -s https://www.cloudflare.com/ips-v{4,6} | sed 's/^/allow /; s/$/;/'; echo; echo "deny all;") > /etc/nginx/cloudflare-only.conf
|
||||
#
|
||||
# and then enable it below by uncomenting the cloudflare-only.conf line
|
||||
# and then enable it below by uncommenting the cloudflare-only.conf line
|
||||
#
|
||||
# ======================================================================
|
||||
|
||||
|
|
|
@ -50,7 +50,9 @@ let
|
|||
|
||||
configStr = ''
|
||||
${mkSection "global" cfg.settings}
|
||||
${cfg.globalExtraConfig}
|
||||
${mkSection "accounts" (accountsWithPlaceholders cfg.accounts)}
|
||||
${mkSection "groups" cfg.groups}
|
||||
${concatStringsSep "\n" (mapAttrsToList mkVolume cfg.volumes)}
|
||||
'';
|
||||
|
||||
|
@ -131,6 +133,12 @@ in
|
|||
'';
|
||||
};
|
||||
|
||||
globalExtraConfig = mkOption {
|
||||
type = types.str;
|
||||
default = "";
|
||||
description = "Appended to the end of the [global] section verbatim. This is useful for flags which are used in a repeating manner (e.g. ipu: 255.255.255.1=user) which can't be repeated in the settings = {} attribute set.";
|
||||
};
|
||||
|
||||
accounts = mkOption {
|
||||
type = types.attrsOf (
|
||||
types.submodule (
|
||||
|
@ -160,6 +168,19 @@ in
|
|||
'';
|
||||
};
|
||||
|
||||
groups = mkOption {
|
||||
type = types.attrsOf (types.listOf types.str);
|
||||
description = ''
|
||||
A set of copyparty groups to create and the users that should be part of each group.
|
||||
'';
|
||||
default = { };
|
||||
example = literalExpression ''
|
||||
{
|
||||
group_name = [ "user1" "user2" ];
|
||||
};
|
||||
'';
|
||||
};
|
||||
|
||||
volumes = mkOption {
|
||||
type = types.attrsOf (
|
||||
types.submodule (
|
||||
|
@ -349,12 +370,16 @@ in
|
|||
) cfg.volumes
|
||||
);
|
||||
|
||||
users.groups.copyparty = lib.mkIf (cfg.user == "copyparty" && cfg.group == "copyparty") { };
|
||||
users.users.copyparty = lib.mkIf (cfg.user == "copyparty" && cfg.group == "copyparty") {
|
||||
description = "Service user for copyparty";
|
||||
group = "copyparty";
|
||||
home = externalStateDir;
|
||||
isSystemUser = true;
|
||||
users.groups = lib.mkIf (cfg.group == "copyparty") {
|
||||
copyparty = { };
|
||||
};
|
||||
users.users = lib.mkIf (cfg.user == "copyparty") {
|
||||
copyparty = {
|
||||
description = "Service user for copyparty";
|
||||
group = cfg.group;
|
||||
home = externalStateDir;
|
||||
isSystemUser = true;
|
||||
};
|
||||
};
|
||||
environment.systemPackages = lib.mkIf cfg.mkHashWrapper [
|
||||
(pkgs.writeShellScriptBin "copyparty-hash" ''
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
# NOTE: You generally shouldn't use this PKGBUILD on Arch, as it is mainly for testing purposes. Install copyparty using pacman instead.
|
||||
|
||||
pkgname=copyparty
|
||||
pkgver="1.19.2"
|
||||
pkgver="1.19.16"
|
||||
pkgrel=1
|
||||
pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++"
|
||||
arch=("any")
|
||||
|
@ -23,7 +23,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}/copyparty.conf" )
|
||||
sha256sums=("9f0dcd8124f260a0c72676b70d84c82388cfe5b47e7d0556f5190c88208580a2")
|
||||
sha256sums=("d8cc10d3623eaccc8acaebdd3b0102a1ebf878a03ffe6e737d132c66f799d682")
|
||||
|
||||
build() {
|
||||
cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web"
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
|
||||
pkgname=copyparty
|
||||
pkgver=1.19.2
|
||||
pkgver=1.19.16
|
||||
pkgrel=1
|
||||
pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++"
|
||||
arch=("any")
|
||||
|
@ -20,7 +20,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=("9f0dcd8124f260a0c72676b70d84c82388cfe5b47e7d0556f5190c88208580a2")
|
||||
sha256sums=("d8cc10d3623eaccc8acaebdd3b0102a1ebf878a03ffe6e737d132c66f799d682")
|
||||
|
||||
build() {
|
||||
cd "${srcdir}/${pkgname}-${pkgver}/copyparty/web"
|
||||
|
|
|
@ -26,7 +26,7 @@ Environment=XDG_CONFIG_HOME=/home/cpp/.config
|
|||
ExecStartPre=+/bin/bash -c 'mkdir -p /run/tmpfiles.d/ && echo "x /tmp/pe-copyparty*" > /run/tmpfiles.d/copyparty.conf'
|
||||
|
||||
# run copyparty
|
||||
ExecStart=/usr/bin/python3 /usr/bin/copyparty -c /etc/copyparty.d/init
|
||||
ExecStart=/usr/bin/python3 /usr/local/bin/copyparty -c /etc/copyparty.d/init
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"url": "https://github.com/9001/copyparty/releases/download/v1.19.2/copyparty-1.19.2.tar.gz",
|
||||
"version": "1.19.2",
|
||||
"hash": "sha256-nw3NgSTyYKDHJna3DYTII4jP5bR+fQVW9RkMiCCFgKI="
|
||||
"url": "https://github.com/9001/copyparty/releases/download/v1.19.16/copyparty-1.19.16.tar.gz",
|
||||
"version": "1.19.16",
|
||||
"hash": "sha256-2MwQ02I+rMyKyuvdOwECoev4eKA//m5zfRMsZveZ1oI="
|
||||
}
|
11
contrib/package/nix/overlay.nix
Normal file
11
contrib/package/nix/overlay.nix
Normal file
|
@ -0,0 +1,11 @@
|
|||
final: prev: {
|
||||
copyparty = final.python3.pkgs.callPackage ./copyparty {
|
||||
ffmpeg = final.ffmpeg-full;
|
||||
};
|
||||
|
||||
python3 = prev.python3.override {
|
||||
packageOverrides = pyFinal: pyPrev: {
|
||||
partftpy = pyFinal.callPackage ./partftpy { };
|
||||
};
|
||||
};
|
||||
}
|
|
@ -101,10 +101,10 @@
|
|||
|
||||
function our_hotkey_handler(e) {
|
||||
// bail if either ALT, CTRL, or SHIFT is pressed
|
||||
if (e.altKey || e.shiftKey || e.isComposing || ctrl(e))
|
||||
if (anymod(e))
|
||||
return main_hotkey_handler(e); // let copyparty handle this keystroke
|
||||
|
||||
var key_name = (e.code || e.key) + '',
|
||||
var keycode = (e.key || e.code) + '',
|
||||
ae = document.activeElement,
|
||||
aet = ae && ae != document.body ? ae.nodeName.toLowerCase() : '';
|
||||
|
||||
|
@ -114,7 +114,7 @@
|
|||
if (aet && !/^(a|button|tr|td|div|pre)$/.test(aet))
|
||||
return main_hotkey_handler(e); // let copyparty handle this keystroke
|
||||
|
||||
if (key_name == 'KeyW') {
|
||||
if (keycode == 'w' || keycode == 'KeyW') {
|
||||
// okay, this one's for us... do the thing
|
||||
action_to_perform();
|
||||
return ev(e);
|
||||
|
|
173
contrib/podman-systemd/README.md
Normal file
173
contrib/podman-systemd/README.md
Normal file
|
@ -0,0 +1,173 @@
|
|||
# copyparty with Podman and Systemd
|
||||
|
||||
Use this configuration if you want to run copyparty in a Podman container, with the reliability of running the container under a systemd service.
|
||||
|
||||
Documentation for `.container` files can be found in the [Container unit](https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html#container-units-container) docs. Systemd does not understand `.container` files natively, so Podman converts these to `.service` files with a [systemd-generator](https://www.freedesktop.org/software/systemd/man/latest/systemd.generator.html). This process is transparent, but sometimes needs to be debugged in case your `.container` file is malformed. There are instructions to debug the systemd generator in the Troubleshooting section below.
|
||||
|
||||
To run copyparty in this way, you must already have podman installed. To install Podman, see: https://podman.io/docs/installation
|
||||
|
||||
There is a sample configuration file in the same directory as this file (`copyparty.conf`).
|
||||
|
||||
## Run the container as root
|
||||
|
||||
Running the container as the root user is easy to set up, but less secure. There are instructions in the next section to run the container as a rootless user if you'd rather run the container like that.
|
||||
|
||||
First, change this line in the `copyparty.container` file to reflect the directory you want to share. By default, it shares `/mnt/` but you'll probably want to change that.
|
||||
|
||||
```
|
||||
# Change /mnt to something you want to share
|
||||
Volume=/mnt:/w:z
|
||||
```
|
||||
|
||||
Note that you can select the owner and group of this volume by changing the `uid:` and `gid:` of the volume in `copyparty.conf`, but for simplicity let's assume you want it to be owned by `root:root`.
|
||||
|
||||
To install and start copyparty with Podman and systemd as the root user, run the following:
|
||||
|
||||
```shell
|
||||
sudo mkdir -pv /etc/systemd/container/ /etc/copyparty/
|
||||
sudo cp -v copyparty.container /etc/systemd/containers/
|
||||
sudo cp -v copyparty.conf /etc/copyparty/
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl start copyparty
|
||||
```
|
||||
|
||||
Note: You can't "enable" this kind of Podman service. The `[Install]` section of the `.container` file effectively handles enabling the service so that it starts when the server reboots.
|
||||
|
||||
You can see the status of the service with:
|
||||
|
||||
```shell
|
||||
sudo systemctl status -a copyparty
|
||||
```
|
||||
|
||||
You can see (and follow) the logs with either of these commands:
|
||||
|
||||
```shell
|
||||
sudo podman logs -f copyparty
|
||||
|
||||
# -a is required or else you'll get output like: copyparty[549025]: [649B blob data]
|
||||
sudo journalctl -a -f -u copyparty
|
||||
```
|
||||
|
||||
## Run the container as a non-root user
|
||||
|
||||
This configuration is more secure, but is more involved and requires ensuring files have proper permissions. You will need a root user account to do some of this setup.
|
||||
|
||||
First, you need a user to run the container as. In this example we'll create a "podman" user with UID=1001 and GID=1001.
|
||||
|
||||
```shell
|
||||
sudo groupadd -g 1001 podman
|
||||
sudo useradd -u 1001 -m podman
|
||||
sudo usermod -aG podman podman
|
||||
sudo loginctl enable-linger podman
|
||||
# Set a strong password for this user
|
||||
sudo -u podman passwd
|
||||
```
|
||||
|
||||
The `enable-linger` command allows the podman user to run systemd user services that persist even when the user is not logged in. You could use a user that already exists in the system to run this service as, just make sure to run `loginctl enable-linger USERNAME` for that user.
|
||||
|
||||
Next, change these lines in the `copyparty.container` file to reflect the config directory and the directory you want to share. By default, the config shares `/home/podman/copyparty/sharing/` but you'll probably want to change this:
|
||||
|
||||
```
|
||||
# Change to reflect your non-root user's home directory
|
||||
Volume=/home/podman/copyparty/config:/cfg:z
|
||||
|
||||
# Change to the directory you want to share
|
||||
Volume=/home/podman/copyparty/sharing:/w:z
|
||||
```
|
||||
|
||||
Make sure the podman user has read/write access to both of these directories.
|
||||
|
||||
Next, **log in to the server as the podman user**.
|
||||
|
||||
To install and start copyparty as the non-root podman user, run the following:
|
||||
|
||||
```shell
|
||||
mkdir -pv /home/podman/.config/containers/systemd/ /home/podman/copyparty/config
|
||||
cp -v copyparty.container /home/podman/.config/containers/systemd/copyparty.container
|
||||
cp -v copyparty.conf /home/podman/copyparty/config
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user start copyparty
|
||||
```
|
||||
|
||||
**Important note: Never use `sudo` with `systemctl --user`!**
|
||||
|
||||
You can check the status of the user service with:
|
||||
|
||||
```shell
|
||||
systemctl --user status -a copyparty
|
||||
```
|
||||
|
||||
You can see (and follow) the logs with:
|
||||
|
||||
```shell
|
||||
podman logs -f copyparty
|
||||
|
||||
journalctl --user -a -f -u copyparty
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If the container fails to start, and you've modified the `.container` service, it's likely that your `.container` file failed to be translated into a `.service` file. You can debug the podman service generator with this command:
|
||||
|
||||
```shell
|
||||
sudo /usr/lib/systemd/system-generators/podman-system-generator --dryrun
|
||||
```
|
||||
|
||||
## Allowing Traffic from Outside your Server
|
||||
|
||||
To allow traffic on port 3923 of your server, you should run:
|
||||
|
||||
```shell
|
||||
sudo firewall-cmd --permanent --add-port=3923/tcp
|
||||
sudo firewall-cmd --reload
|
||||
```
|
||||
|
||||
Otherwise, you won't be able to access the copyparty server from anywhere other than the server itself.
|
||||
|
||||
## Updating copyparty
|
||||
|
||||
To update the version of copyparty used in the container, you can:
|
||||
|
||||
```shell
|
||||
# If root:
|
||||
sudo podman pull docker.io/copyparty/ac:latest
|
||||
sudo systemctl restart copyparty
|
||||
|
||||
# If non-root:
|
||||
podman pull docker.io/copyparty/ac:latest
|
||||
systemctl --user restart copyparty
|
||||
```
|
||||
|
||||
Or, you can change the pinned version of the image in the `[Container]` section of the `.container` file and run:
|
||||
|
||||
```shell
|
||||
# If root:
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl restart copyparty
|
||||
|
||||
# If non-root:
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user restart copyparty
|
||||
```
|
||||
|
||||
Podman will pull the image you've specified when restarting. If you have it set to `:latest`, Podman does not know to re-pull the container.
|
||||
|
||||
### Enabling auto-update
|
||||
|
||||
Alternatively, you can enable auto-updates by un-commenting this line:
|
||||
|
||||
```
|
||||
# AutoUpdate=registry
|
||||
```
|
||||
|
||||
You will also need to enable the [podman auto-updater service](https://docs.podman.io/en/latest/markdown/podman-auto-update.1.html) with:
|
||||
|
||||
```shell
|
||||
# If root:
|
||||
sudo systemctl enable podman-auto-update.timer podman-auto-update.service
|
||||
|
||||
# If non-root:
|
||||
systemctl --user enable podman-auto-update.timer podman-auto-update.service
|
||||
```
|
||||
|
||||
This works best if you always want the latest version of copyparty. The auto-updater runs once every 24 hours.
|
36
contrib/podman-systemd/copyparty.conf
Normal file
36
contrib/podman-systemd/copyparty.conf
Normal file
|
@ -0,0 +1,36 @@
|
|||
[global]
|
||||
e2dsa # enable file indexing and filesystem scanning
|
||||
e2ts # and enable multimedia indexing
|
||||
ansi # and colors in log messages
|
||||
|
||||
# uncomment the line starting with q, lo: to log to a file instead of stdout/journalctl;
|
||||
# $LOGS_DIRECTORY is usually /var/log/copyparty (comes from systemd)
|
||||
# and copyparty replaces %Y-%m%d with Year-MonthDay, so the
|
||||
# full path will be something like /var/log/copyparty/2023-1130.txt
|
||||
# (note: enable compression by adding .xz at the end)
|
||||
# q, lo: $LOGS_DIRECTORY/%Y-%m%d.log
|
||||
|
||||
# p: 80,443,3923 # listen on 80/443 as well (requires CAP_NET_BIND_SERVICE)
|
||||
# i: 127.0.0.1 # only allow connections from localhost (reverse-proxies)
|
||||
# ftp: 3921 # enable ftp server on port 3921
|
||||
# p: 3939 # listen on another port
|
||||
# df: 16 # stop accepting uploads if less than 16 GB free disk space
|
||||
# ver # show copyparty version in the controlpanel
|
||||
# grid # show thumbnails/grid-view by default
|
||||
# theme: 2 # monokai
|
||||
# name: datasaver # change the server-name that's displayed in the browser
|
||||
# stats, nos-dup # enable the prometheus endpoint, but disable the dupes counter (too slow)
|
||||
# no-robots, force-js # make it harder for search engines to read your server
|
||||
|
||||
|
||||
[accounts]
|
||||
ed: wark # username: password
|
||||
|
||||
|
||||
[/] # create a volume at "/" (the webroot), which will
|
||||
/w # share the contents of the "/w" folder
|
||||
accs:
|
||||
rw: * # everyone gets read-write access, but
|
||||
rwmda: ed # the user "ed" gets read-write-move-delete-admin
|
||||
# uid: 1000 # If you're running as root, you can change the owner of this volume here
|
||||
# gid: 1000 # If you're running as root, you can change the group of this volume here
|
55
contrib/podman-systemd/copyparty.container
Normal file
55
contrib/podman-systemd/copyparty.container
Normal file
|
@ -0,0 +1,55 @@
|
|||
[Container]
|
||||
# It's recommended to replace :latest with a specific version
|
||||
# for example: docker.io/copyparty/ac:1.19.15
|
||||
Image=docker.io/copyparty/ac:latest
|
||||
ContainerName=copyparty
|
||||
|
||||
# Uncomment to enable auto-updates
|
||||
# AutoUpdate=registry
|
||||
|
||||
# Environment variables
|
||||
# enable mimalloc by replacing "NOPE" with "2" for a nice speed-boost (will use twice as much ram)
|
||||
Environment=LD_PRELOAD=/usr/lib/libmimalloc-secure.so.NOPE
|
||||
# ensures log-messages are not delayed (but can reduce speed a tiny bit)
|
||||
Environment=PYTHONUNBUFFERED=1
|
||||
|
||||
# Ports
|
||||
PublishPort=3923:3923
|
||||
|
||||
|
||||
# Volumes (PLEASE LOOK!)
|
||||
|
||||
# Rootful setup:
|
||||
# Leave as-is
|
||||
# Non-root setup:
|
||||
# Change /etc/copyparty to /home/<USER>/copyparty/config
|
||||
Volume=/etc/copyparty:/cfg:z
|
||||
|
||||
# Rootful setup:
|
||||
# Change /mnt to the directory you want to share
|
||||
# Non-root setup:
|
||||
# Change /mnt to something owned by your user, e.g., /home/<USER>/copyparty/sharing:/w:z
|
||||
Volume=/mnt:/w:z
|
||||
|
||||
|
||||
# Give the container time to stop in case the thumbnailer is still running.
|
||||
# It's allowed to continue finishing up for 10s after the shutdown signal, give it a 5s buffer
|
||||
StopTimeout=15
|
||||
|
||||
# hide it from logs with "/._" so it matches the default --lf-url filter
|
||||
HealthCmd="wget --spider -q 127.0.0.1:3923/?reset=/._"
|
||||
HealthInterval=1m
|
||||
HealthTimeout=2s
|
||||
HealthRetries=5
|
||||
HealthStartPeriod=15s
|
||||
|
||||
[Unit]
|
||||
After=default.target
|
||||
|
||||
[Install]
|
||||
# Start by default on boot
|
||||
WantedBy=default.target
|
||||
|
||||
[Service]
|
||||
# Give the container time to start in case it needs to pull the image
|
||||
TimeoutStartSec=600
|
71
contrib/setup-ashell.sh
Normal file
71
contrib/setup-ashell.sh
Normal file
|
@ -0,0 +1,71 @@
|
|||
#!/bin/bash
|
||||
#
|
||||
# this script will install copyparty onto an iOS device (iPhone/iPad)
|
||||
#
|
||||
# step 1: install a-Shell:
|
||||
# https://apps.apple.com/us/app/a-shell/id1473805438
|
||||
#
|
||||
# step 2: copypaste the following command into a-Shell:
|
||||
# curl -L https://github.com/9001/copyparty/raw/refs/heads/hovudstraum/contrib/setup-ashell.sh
|
||||
#
|
||||
# step 3: launch copyparty with this command: cpp
|
||||
#
|
||||
# if you ever want to upgrade copyparty, just repeat step 2
|
||||
|
||||
|
||||
|
||||
cd "$HOME/Documents"
|
||||
curl -Locopyparty https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py
|
||||
|
||||
|
||||
|
||||
# create the config file? (cannot use heredoc because body too large)
|
||||
[ -e cpc ] || {
|
||||
echo '[global]' >cpc
|
||||
echo ' p: 80, 443, 3923 # enable http and https on these ports' >>cpc
|
||||
echo ' e2dsa # enable file indexing and filesystem scanning' >>cpc
|
||||
echo ' e2ts # and enable multimedia indexing' >>cpc
|
||||
echo ' ver # show copyparty version in the controlpanel' >>cpc
|
||||
echo ' qrz: 2 # enable qr-code and make it big' >>cpc
|
||||
echo ' qrp: 1 # reduce qr-code padding' >>cpc
|
||||
echo ' qr-fg: -1 # optimize for basic/simple terminals' >>cpc
|
||||
echo ' qr-wait: 0.3 # less chance of getting scrolled away' >>cpc
|
||||
echo '' >>cpc
|
||||
echo ' # enable these by uncommenting them:' >>cpc
|
||||
echo ' # ftp: 21 # enable ftp server on port 21' >>cpc
|
||||
echo ' # tftp: 69 # enable tftp server on port 69' >>cpc
|
||||
echo '' >>cpc
|
||||
echo '[/]' >>cpc
|
||||
echo ' ~/Documents' >>cpc
|
||||
echo ' accs:' >>cpc
|
||||
echo ' A: *' >>cpc
|
||||
}
|
||||
|
||||
|
||||
|
||||
# create the launcher?
|
||||
[ -e cpp ] || {
|
||||
echo '#!/bin/sh' >cpp
|
||||
echo '' >>cpp
|
||||
echo '# change the font so the qr-code draws correctly:' >>cpp
|
||||
echo 'config -n "Menlo" # name' >>cpp
|
||||
echo 'config -s 8 # size' >>cpp
|
||||
echo '' >>cpp
|
||||
echo '# launch copyparty' >>cpp
|
||||
echo 'exec copyparty -c cpc "$@"' >>cpp
|
||||
}
|
||||
|
||||
|
||||
|
||||
chmod 755 copyparty cpp
|
||||
echo
|
||||
echo =================================
|
||||
echo
|
||||
echo 'okay, all done!'
|
||||
echo
|
||||
echo 'you can edit your config'
|
||||
echo 'with this command: vim cpc'
|
||||
echo
|
||||
echo 'you can run copyparty'
|
||||
echo 'with this command: cpp'
|
||||
echo
|
|
@ -88,6 +88,7 @@ web/mde.html
|
|||
web/mde.js
|
||||
web/msg.css
|
||||
web/msg.html
|
||||
web/opds.xml
|
||||
web/rups.css
|
||||
web/rups.html
|
||||
web/rups.js
|
||||
|
@ -111,7 +112,9 @@ class EnvParams(object):
|
|||
def __init__(self) -> None:
|
||||
self.t0 = time.time()
|
||||
self.mod = ""
|
||||
self.mod_ = ""
|
||||
self.cfg = ""
|
||||
self.scfg = True
|
||||
|
||||
|
||||
E = EnvParams()
|
||||
|
|
|
@ -36,6 +36,7 @@ from .__init__ import (
|
|||
)
|
||||
from .__version__ import CODENAME, S_BUILD_DT, S_VERSION
|
||||
from .authsrv import expand_config_file, split_cfg_ln, upgrade_cfg_fmt
|
||||
from .bos import bos
|
||||
from .cfg import flagcats, onedash
|
||||
from .svchub import SvcHub
|
||||
from .util import (
|
||||
|
@ -186,7 +187,7 @@ def init_E(EE: EnvParams) -> None:
|
|||
|
||||
E = EE # pylint: disable=redefined-outer-name
|
||||
|
||||
def get_unixdir() -> str:
|
||||
def get_unixdir() -> tuple[str, bool]:
|
||||
paths: list[tuple[Callable[..., Any], str]] = [
|
||||
(os.environ.get, "XDG_CONFIG_HOME"),
|
||||
(os.path.expanduser, "~/.config"),
|
||||
|
@ -196,41 +197,57 @@ def init_E(EE: EnvParams) -> None:
|
|||
(unicode, "/tmp"),
|
||||
]
|
||||
errs = []
|
||||
for chk in [os.listdir, os.mkdir]:
|
||||
for npath, (pf, pa) in enumerate(paths):
|
||||
p = ""
|
||||
for npath, (pf, pa) in enumerate(paths):
|
||||
priv = npath < 2 # private/trusted location
|
||||
ram = npath > 1 # "nonvolatile"; not semantically same as `not priv`
|
||||
p = ""
|
||||
try:
|
||||
p = pf(pa)
|
||||
if not p or p.startswith("~"):
|
||||
continue
|
||||
|
||||
p = os.path.normpath(p)
|
||||
mkdir = not os.path.isdir(p)
|
||||
if mkdir:
|
||||
os.mkdir(p, 0o700)
|
||||
|
||||
p = os.path.join(p, "copyparty")
|
||||
if not priv and os.path.isdir(p):
|
||||
uid = os.geteuid()
|
||||
if os.stat(p).st_uid != uid:
|
||||
p += ".%s" % (uid,)
|
||||
if os.path.isdir(p) and os.stat(p).st_uid != uid:
|
||||
raise Exception("filesystem has broken unix permissions")
|
||||
try:
|
||||
p = pf(pa)
|
||||
# print(chk.__name__, p, pa)
|
||||
if not p or p.startswith("~"):
|
||||
continue
|
||||
os.listdir(p)
|
||||
except:
|
||||
os.mkdir(p, 0o700)
|
||||
|
||||
p = os.path.normpath(p)
|
||||
chk(p) # type: ignore
|
||||
p = os.path.join(p, "copyparty")
|
||||
if not os.path.isdir(p):
|
||||
os.mkdir(p)
|
||||
if ram:
|
||||
t = "Using %s/copyparty [%s] for config; filekeys/dirkeys will change on every restart. Consider setting XDG_CONFIG_HOME or giving the unix-user a ~/.config/"
|
||||
errs.append(t % (pa, p))
|
||||
elif mkdir:
|
||||
t = "Using %s/copyparty [%s] for config%s (Warning: %s did not exist and was created just now)"
|
||||
errs.append(t % (pa, p, " instead" if npath else "", pa))
|
||||
elif errs:
|
||||
errs.append("Using %s/copyparty [%s] instead" % (pa, p))
|
||||
|
||||
if npath > 1:
|
||||
t = "Using [%s] for config; filekeys/dirkeys will change on every restart. Consider setting XDG_CONFIG_HOME or giving the unix-user a ~/.config/"
|
||||
errs.append(t % (p,))
|
||||
elif errs:
|
||||
errs.append("Using [%s] instead" % (p,))
|
||||
if errs:
|
||||
warn(". ".join(errs))
|
||||
|
||||
if errs:
|
||||
warn(". ".join(errs))
|
||||
return p, priv
|
||||
except Exception as ex:
|
||||
if p:
|
||||
t = "Unable to store config in %s [%s] due to %r"
|
||||
errs.append(t % (pa, p, ex))
|
||||
|
||||
return p # type: ignore
|
||||
except Exception as ex:
|
||||
if p and npath < 2:
|
||||
t = "Unable to store config in [%s] due to %r"
|
||||
errs.append(t % (p, ex))
|
||||
|
||||
raise Exception("could not find a writable path for config")
|
||||
t = "could not find a writable path for runtime state:\n> %s"
|
||||
raise Exception(t % ("\n> ".join(errs)))
|
||||
|
||||
E.mod = os.path.dirname(os.path.realpath(__file__))
|
||||
if E.mod.endswith("__init__"):
|
||||
E.mod = os.path.dirname(E.mod)
|
||||
E.mod_ = os.path.join(E.mod, "")
|
||||
|
||||
try:
|
||||
p = os.environ.get("XDG_CONFIG_HOME")
|
||||
|
@ -241,7 +258,7 @@ def init_E(EE: EnvParams) -> None:
|
|||
p = os.path.abspath(os.path.realpath(p))
|
||||
p = os.path.join(p, "copyparty")
|
||||
if not os.path.isdir(p):
|
||||
os.mkdir(p)
|
||||
os.mkdir(p, 0o700)
|
||||
os.listdir(p)
|
||||
except:
|
||||
p = ""
|
||||
|
@ -254,11 +271,11 @@ def init_E(EE: EnvParams) -> None:
|
|||
elif sys.platform == "darwin":
|
||||
E.cfg = os.path.expanduser("~/Library/Preferences/copyparty")
|
||||
else:
|
||||
E.cfg = get_unixdir()
|
||||
E.cfg, E.scfg = get_unixdir()
|
||||
|
||||
E.cfg = E.cfg.replace("\\", "/")
|
||||
try:
|
||||
os.makedirs(E.cfg)
|
||||
bos.makedirs(E.cfg, bos.MKD_700)
|
||||
except:
|
||||
if not os.path.isdir(E.cfg):
|
||||
raise
|
||||
|
@ -439,7 +456,7 @@ def args_from_cfg(cfg_path: str) -> list[str]:
|
|||
def expand_cfg(argv) -> list[str]:
|
||||
if CFG_DEF:
|
||||
supp = args_from_cfg(CFG_DEF[0])
|
||||
argv = supp + argv
|
||||
argv = argv[:1] + supp + argv[1:]
|
||||
|
||||
n = 0
|
||||
while n < len(argv):
|
||||
|
@ -670,6 +687,42 @@ def get_sects():
|
|||
"""
|
||||
),
|
||||
],
|
||||
[
|
||||
"auth-ord",
|
||||
"authentication precedence",
|
||||
dedent(
|
||||
"""
|
||||
\033[33m--auth-ord\033[0m is a comma-separated list of auth options
|
||||
(one or more of the [\033[35moptions\033[0m] below); first one wins
|
||||
|
||||
[\033[35mpw\033[0m] is conventional login, for example the "\033[36mPW\033[0m" header,
|
||||
or the \033[36m?pw=\033[0m[...] URL-suffix, or a valid session cookie
|
||||
(see \033[33m--help-auth\033[0m)
|
||||
|
||||
[\033[35midp\033[0m] is a username provided in the http-request-header
|
||||
defined by \033[33m--idp-h-usr\033[0m and/or \033[33m--idp-hm-usr\033[0m, which is
|
||||
provided by an authentication middleware such as
|
||||
authentik, authelia, tailscale, ... (see \033[33m--help-idp\033[0m)
|
||||
|
||||
[\033[35midp-h\033[0m] is specifically an \033[33m--idp-h-usr\033[0m header,
|
||||
[\033[35midp-hm\033[0m] is specifically an \033[33m--idp-hm-usr\033[0m header;
|
||||
[\033[35midp\033[0m] is the same as [\033[35midp-hm,idp-h\033[0m]
|
||||
|
||||
[\033[35mipu\033[0m] is a mapping from an IP-address to a username,
|
||||
auto-authing that client-IP to that account
|
||||
(see the description of \033[36m--ipu\033[0m in \033[33m--help\033[0m)
|
||||
|
||||
NOTE: even if an option (\033[35mpw\033[0m/\033[35mipu\033[0m/...) is not in the list,
|
||||
it may still be enabled and can still take effect if
|
||||
none of the other alternatives identify the user
|
||||
|
||||
NOTE: if [\033[35mipu\033[0m] is in the list, it must be FIRST or LAST
|
||||
|
||||
NOTE: if [\033[35mpw\033[0m] is not in the list, the logout-button
|
||||
will be hidden when any idp feature is enabled
|
||||
"""
|
||||
),
|
||||
],
|
||||
[
|
||||
"flags",
|
||||
"list of volflags",
|
||||
|
@ -760,9 +813,11 @@ def get_sects():
|
|||
\033[36mf\033[35m forks the process, doesn't wait for completion
|
||||
\033[36mc\033[35m checks return code, blocks the action if non-zero
|
||||
\033[36mj\033[35m provides json with info as 1st arg instead of filepath
|
||||
\033[36ms\033[35m provides input data on stdin (instead of 1st arg)
|
||||
\033[36mwN\033[35m waits N sec after command has been started before continuing
|
||||
\033[36mtN\033[35m sets an N sec timeout before the command is abandoned
|
||||
\033[36miN\033[35m xiu only: volume must be idle for N sec (default = 5)
|
||||
\033[36mI\033[35m import and run as module, not as subprocess
|
||||
|
||||
\033[36mar\033[35m only run hook if user has read-access
|
||||
\033[36marw\033[35m only run hook if user has read-write-access
|
||||
|
@ -775,7 +830,7 @@ def get_sects():
|
|||
\033[36mc0\033[35m show all process output (default)
|
||||
\033[36mc1\033[35m show only stderr
|
||||
\033[36mc2\033[35m show only stdout
|
||||
\033[36mc3\033[35m mute all process otput
|
||||
\033[36mc3\033[35m mute all process output
|
||||
\033[0m
|
||||
examples:
|
||||
|
||||
|
@ -792,6 +847,9 @@ def get_sects():
|
|||
the \033[33m--\033[35m stops notify-send from reading the message as args
|
||||
and the alert will be "hey" followed by the messagetext
|
||||
|
||||
\033[36m--xm s,,tee,-a,log.txt\033[35m appends each msg to log.txt;
|
||||
\033[36m--xm s,j,,tee,-a,log.txt\033[35m writes it as json instead
|
||||
|
||||
\033[36m--xau zmq:pub:tcp://*:5556\033[35m announces uploads on zeromq;
|
||||
\033[36m--xau t3,zmq:push:tcp://*:5557\033[35m also works, and you can
|
||||
\033[36m--xau t3,j,zmq:req:tcp://localhost:5555\033[35m too for example
|
||||
|
@ -801,7 +859,8 @@ def get_sects():
|
|||
as soon as the volume has been idle for iN seconds (5 by default)
|
||||
|
||||
\033[36mxiu\033[0m is also unique in that it will pass the metadata to the
|
||||
executed program on STDIN instead of as argv arguments, and
|
||||
executed program on STDIN instead of as argv arguments (so
|
||||
just like the \033[36ms\033[0m option does for the other hook types), and
|
||||
it also includes the wark (file-id/hash) as a json property
|
||||
|
||||
\033[36mxban\033[0m can be used to overrule / cancel a user ban event;
|
||||
|
@ -812,6 +871,12 @@ def get_sects():
|
|||
on new uploads, but with certain limitations. See
|
||||
bin/hooks/reloc* and docs/devnotes.md#hook-effects
|
||||
|
||||
the \033[36mI\033[0m option will override most other options, because
|
||||
it entirely hands over control to the hook, which is
|
||||
then able to tamper with copyparty's internal memory
|
||||
and wreck havoc if it wants to -- but this is worh it
|
||||
because it makes the hook 140x faster
|
||||
|
||||
except for \033[36mxm\033[0m, only one hook / one action can run at a time,
|
||||
so it's recommended to use the \033[36mf\033[0m flag unless you really need
|
||||
to wait for the hook to finish before continuing (without \033[36mf\033[0m
|
||||
|
@ -845,6 +910,11 @@ def get_sects():
|
|||
middleware and not by clients! and, as an extra precaution,
|
||||
send a header named '\033[36mfinalmasterspark\033[0m' (a secret keyword)
|
||||
and then \033[36m--idp-h-key finalmasterspark\033[0m to require that
|
||||
|
||||
the login/logout links/buttons can be replaced with links
|
||||
going to your IdP's UI; \033[36m--idp-login /login/?redir={dst}\033[0m
|
||||
will expand \033[36m{dst}\033[0m to the URL of the current page, so
|
||||
the IdP can redirect the user back to where they were
|
||||
"""
|
||||
),
|
||||
],
|
||||
|
@ -898,7 +968,7 @@ def get_sects():
|
|||
\033[36m{{vf.thsize}} \033[35mthumbnail size
|
||||
\033[36m{{srv.itime}} \033[35mserver time in seconds
|
||||
\033[36m{{srv.htime}} \033[35mserver time as YY-mm-dd, HH:MM:SS (UTC)
|
||||
\033[36m{{hdr.cf_ipcountry}} \033[35mthe "CF-IPCountry" client header (probably blank)
|
||||
\033[36m{{hdr.cf-ipcountry}} \033[35mthe "CF-IPCountry" client header (probably blank)
|
||||
\033[0m
|
||||
so the following types of placeholders can be added to the lists:
|
||||
* any client header can be accessed through {{hdr.*}}
|
||||
|
@ -1092,10 +1162,13 @@ def add_general(ap, nc, srvname):
|
|||
ap2.add_argument("-v", metavar="VOL", type=u, action="append", help="\033[34mREPEATABLE:\033[0m add volume, \033[33mSRC\033[0m:\033[33mDST\033[0m:\033[33mFLAG\033[0m; examples [\033[32m.::r\033[0m], [\033[32m/mnt/nas/music:/music:r:aed\033[0m], see --help-accounts")
|
||||
ap2.add_argument("--grp", metavar="G:N,N", type=u, action="append", help="\033[34mREPEATABLE:\033[0m add group, \033[33mNAME\033[0m:\033[33mUSER1\033[0m,\033[33mUSER2\033[0m,\033[33m...\033[0m; example [\033[32madmins:ed,foo,bar\033[0m]")
|
||||
ap2.add_argument("--usernames", action="store_true", help="require username and password for login; default is just password")
|
||||
ap2.add_argument("--chdir", metavar="PATH", type=u, help="change working-directory to \033[33mPATH\033[0m before mapping volumes")
|
||||
ap2.add_argument("-ed", action="store_true", help="enable the ?dots url parameter / client option which allows clients to see dotfiles / hidden files (volflag=dots)")
|
||||
ap2.add_argument("--urlform", metavar="MODE", type=u, default="print,xm", 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("--name-url", metavar="TXT", type=u, help="URL for server name hyperlink (displayed topleft in browser)")
|
||||
ap2.add_argument("--name-html", type=u, help=argparse.SUPPRESS)
|
||||
ap2.add_argument("--mime", metavar="EXT=MIME", type=u, action="append", help="\033[34mREPEATABLE:\033[0m 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("--rmagic", action="store_true", help="do expensive analysis to improve accuracy of returned mimetypes; will make file-downloads, rss, and webdav slower (volflag=rmagic)")
|
||||
|
@ -1114,11 +1187,18 @@ def add_qr(ap, tty):
|
|||
ap2.add_argument("--qrp", metavar="CELLS", type=int, default=4, help="padding (spec says 4 or more, but 1 is usually fine)")
|
||||
ap2.add_argument("--qrz", metavar="N", type=int, default=0, help="[\033[32m1\033[0m]=1x, [\033[32m2\033[0m]=2x, [\033[32m0\033[0m]=auto (try [\033[32m2\033[0m] on broken fonts)")
|
||||
ap2.add_argument("--qr-pin", metavar="N", type=int, default=0, help="sticky/pin the qr-code to always stay on-screen; [\033[32m0\033[0m]=disabled, [\033[32m1\033[0m]=with-url, [\033[32m2\033[0m]=just-qr")
|
||||
ap2.add_argument("--qr-wait", metavar="SEC", type=float, default=0, help="wait \033[33mSEC\033[0m before printing the qr-code to the log")
|
||||
ap2.add_argument("--qr-every", metavar="SEC", type=float, default=0, help="print the qr-code every \033[33mSEC\033[0m (try this with/without --qr-pin in case of issues)")
|
||||
ap2.add_argument("--qr-winch", metavar="SEC", type=float, default=0, help="when --qr-pin is enabled, check for terminal size change every \033[33mSEC\033[0m")
|
||||
ap2.add_argument("--qr-file", metavar="TXT", type=u, action="append", help="\033[34mREPEATABLE:\033[0m write qr-code to file.\n └─To create txt or svg, \033[33mTXT\033[0m is Filepath:Zoom:Pad, for example [\033[32mqr.txt:1:2\033[0m]\n └─To create png or gif, \033[33mTXT\033[0m is Filepath:Zoom:Pad:Foreground:Background, for example [\033[32mqr.png:8:2:333333:ffcc55\033[0m], or [\033[32mqr.png:8:2::ffcc55\033[0m] for transparent")
|
||||
ap2.add_argument("--qr-stdout", action="store_true", help="always display the QR-code on STDOUT in the terminal, even if \033[33m-q\033[0m")
|
||||
ap2.add_argument("--qr-stderr", action="store_true", help="always display the QR-code on STDERR in the terminal, even if \033[33m-q\033[0m")
|
||||
|
||||
|
||||
def add_fs(ap):
|
||||
ap2 = ap.add_argument_group("filesystem options")
|
||||
rm_re_def = "15/0.1" if ANYWIN else "0/0"
|
||||
ap2.add_argument("--casechk", metavar="N", type=u, default="auto", help="detect and prevent CI (case-insensitive) behavior if the underlying filesystem is CI? [\033[32my\033[0m] = detect and prevent, [\033[32mn\033[0m] = ignore and allow, [\033[32mauto\033[0m] = \033[32my\033[0m if CI fs detected. NOTE: \033[32my\033[0m is very slow but necessary for correct WebDAV behavior on Windows/Macos (volflag=casechk)")
|
||||
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)")
|
||||
|
@ -1130,6 +1210,7 @@ def add_share(ap):
|
|||
ap2 = ap.add_argument_group("share-url options")
|
||||
ap2.add_argument("--shr", metavar="DIR", type=u, default="", help="toplevel virtual folder for shared files/folders, for example [\033[32m/share\033[0m]")
|
||||
ap2.add_argument("--shr-db", metavar="FILE", type=u, default=db_path, help="database to store shares in")
|
||||
ap2.add_argument("--shr-who", metavar="TXT", type=u, default="auth", help="who can create a share? [\033[32mno\033[0m]=nobody, [\033[32ma\033[0m]=admin-permission, [\033[32mauth\033[0m]=authenticated (volflag=shr_who)")
|
||||
ap2.add_argument("--shr-adm", metavar="U,U", type=u, default="", help="comma-separated list of users allowed to view/delete any share")
|
||||
ap2.add_argument("--shr-rt", metavar="MIN", type=int, default=1440, help="shares can be revived by their owner if they expired less than MIN minutes ago; [\033[32m60\033[0m]=hour, [\033[32m1440\033[0m]=day, [\033[32m10080\033[0m]=week")
|
||||
ap2.add_argument("--shr-v", action="store_true", help="debug")
|
||||
|
@ -1153,6 +1234,7 @@ def add_upload(ap):
|
|||
ap2.add_argument("--chmod-d", metavar="UGO", type=u, default="755", help="unix file permissions to use when creating directories; see --help-chmod. Examples: [\033[32m755\033[0m] = owner-RW + all-R, [\033[32m777\033[0m] = full-yolo (volflag=chmod_d)")
|
||||
ap2.add_argument("--uid", metavar="N", type=int, default=-1, help="unix user-id to chown new files/folders to; default = -1 = do-not-change (volflag=uid)")
|
||||
ap2.add_argument("--gid", metavar="N", type=int, default=-1, help="unix group-id to chown new files/folders to; default = -1 = do-not-change (volflag=gid)")
|
||||
ap2.add_argument("--wram", action="store_true", help="allow uploading even if a volume is inside a ramdisk, meaning that all data will be lost on the next server reboot (volflag=wram)")
|
||||
ap2.add_argument("--dedup", action="store_true", help="enable symlink-based upload deduplication (volflag=dedup)")
|
||||
ap2.add_argument("--safe-dedup", metavar="N", type=int, default=50, help="how careful to be when deduplicating files; [\033[32m1\033[0m] = just verify the filesize, [\033[32m50\033[0m] = verify file contents have not been altered (volflag=safededup)")
|
||||
ap2.add_argument("--hardlink", action="store_true", help="enable hardlink-based dedup; will fallback on symlinks when that is impossible (across filesystems) (volflag=hardlink)")
|
||||
|
@ -1163,7 +1245,9 @@ def add_upload(ap):
|
|||
ap2.add_argument("--no-snap", action="store_true", help="disable snapshots -- forget unfinished uploads on shutdown; don't create .hist/up2k.snap files -- abandoned/interrupted uploads must be cleaned up manually")
|
||||
ap2.add_argument("--snap-wri", metavar="SEC", type=int, default=300, help="write upload state to ./hist/up2k.snap every \033[33mSEC\033[0m seconds; allows resuming incomplete uploads after a server crash")
|
||||
ap2.add_argument("--snap-drop", metavar="MIN", type=float, default=1440.0, help="forget unfinished uploads after \033[33mMIN\033[0m minutes; impossible to resume them after that (360=6h, 1440=24h)")
|
||||
ap2.add_argument("--rm-partial", action="store_true", help="delete the .PARTIAL file when an unfinished upload expires after \033[33m--snap-drop\033[0m (volflag=rm_partial)")
|
||||
ap2.add_argument("--u2ts", metavar="TXT", type=u, default="c", help="how to timestamp uploaded files; [\033[32mc\033[0m]=client-last-modified, [\033[32mu\033[0m]=upload-time, [\033[32mfc\033[0m]=force-c, [\033[32mfu\033[0m]=force-u (volflag=u2ts)")
|
||||
ap2.add_argument("--rotf-tz", metavar="TXT", type=u, default="UTC", help="default timezone for the rotf upload rule; examples: [\033[32mEurope/Oslo\033[0m], [\033[32mAmerica/Toronto\033[0m], [\033[32mAntarctica/South_Pole\033[0m] (volflag=rotf_tz)")
|
||||
ap2.add_argument("--rand", action="store_true", help="force randomized filenames, \033[33m--nrand\033[0m chars long (volflag=rand)")
|
||||
ap2.add_argument("--nrand", metavar="NUM", type=int, default=9, help="randomized filenames length (volflag=nrand)")
|
||||
ap2.add_argument("--magic", action="store_true", help="enable filetype detection on nameless uploads (volflag=magic)")
|
||||
|
@ -1188,6 +1272,7 @@ def add_network(ap):
|
|||
ap2.add_argument("--xff-src", metavar="CIDR", type=u, default="127.0.0.0/8, ::1/128", help="list of trusted reverse-proxy CIDRs (comma-separated); only accept the real-ip header (\033[33m--xff-hdr\033[0m) and IdP headers if the incoming connection is from an IP within either of these subnets. Specify [\033[32mlan\033[0m] to allow all LAN / private / non-internet IPs. Can be disabled with [\033[32many\033[0m] if you are behind cloudflare (or similar) and are using \033[32m--xff-hdr=cf-connecting-ip\033[0m (or similar)")
|
||||
ap2.add_argument("--ipa", metavar="CIDR", type=u, default="", help="only accept connections from IP-addresses inside \033[33mCIDR\033[0m (comma-separated); examples: [\033[32mlan\033[0m] or [\033[32m10.89.0.0/16, 192.168.33.0/24\033[0m]")
|
||||
ap2.add_argument("--rp-loc", metavar="PATH", type=u, default="", help="if reverse-proxying on a location instead of a dedicated domain/subdomain, provide the base location here; example: [\033[32m/foo/bar\033[0m]")
|
||||
ap2.add_argument("--http-no-tcp", action="store_true", help="do not listen on TCP/IP for http/https; only listen on unix-domain-sockets")
|
||||
if ANYWIN:
|
||||
ap2.add_argument("--reuseaddr", action="store_true", help="set reuseaddr on listening sockets on windows; allows rapid restart of copyparty at the expense of being able to accidentally start multiple instances")
|
||||
elif not MACOS:
|
||||
|
@ -1238,7 +1323,7 @@ def add_auth(ap):
|
|||
ses_db = os.path.join(E.cfg, "sessions.db")
|
||||
ap2 = ap.add_argument_group("IdP / identity provider / user authentication options")
|
||||
ap2.add_argument("--idp-h-usr", metavar="HN", type=u, action="append", help="\033[34mREPEATABLE:\033[0m bypass the copyparty authentication checks if the request-header \033[33mHN\033[0m contains a username to associate the request with (for use with authentik/oauth/...)\n\033[1;31mWARNING:\033[0m if you enable this, make sure clients are unable to specify this header themselves; must be washed away and replaced by a reverse-proxy")
|
||||
ap2.add_argument("--idp-hm-usr", metavar="TXT", type=u, action="append", help="\033[34mREPEATABLE:\033[0m bypass the copyparty authentication checks if the request-header \033[33mHN\033[0m is provided, and its value exists in a mapping defined by this option; see --help-idp")
|
||||
ap2.add_argument("--idp-hm-usr", metavar="T", type=u, action="append", help="\033[34mREPEATABLE:\033[0m bypass the copyparty authentication checks if the request-header \033[33mT\033[0m is provided, and its value exists in a mapping defined by this option; see --help-idp")
|
||||
ap2.add_argument("--idp-h-grp", metavar="HN", type=u, default="", help="assume the request-header \033[33mHN\033[0m contains the groupname of the requesting user; can be referenced in config files for group-based access control")
|
||||
ap2.add_argument("--idp-h-key", metavar="HN", type=u, default="", help="optional but recommended safeguard; your reverse-proxy will insert a secret header named \033[33mHN\033[0m into all requests, and the other IdP headers will be ignored if this header is not present")
|
||||
ap2.add_argument("--idp-gsep", metavar="RE", type=u, default="|:;+,", help="if there are multiple groups in \033[33m--idp-h-grp\033[0m, they are separated by one of the characters in \033[33mRE\033[0m")
|
||||
|
@ -1246,6 +1331,10 @@ def add_auth(ap):
|
|||
ap2.add_argument("--idp-store", metavar="N", type=int, default=1, help="how to use \033[33m--idp-db\033[0m; [\033[32m0\033[0m] = entirely disable, [\033[32m1\033[0m] = write-only (effectively disabled), [\033[32m2\033[0m] = remember users, [\033[32m3\033[0m] = remember users and groups.\nNOTE: Will remember and restore the IdP-volumes of all users for all eternity if set to 2 or 3, even when user is deleted from your IdP")
|
||||
ap2.add_argument("--idp-adm", metavar="U,U", type=u, default="", help="comma-separated list of users allowed to use /?idp (the cache management UI)")
|
||||
ap2.add_argument("--idp-cookie", metavar="S", type=int, default=0, help="generate a session-token for IdP users which is written to cookie \033[33mcppws\033[0m (or \033[33mcppwd\033[0m if plaintext), to reduce the load on the IdP server, lifetime \033[33mS\033[0m seconds.\n └─note: The expiration time is a client hint only; the actual lifetime of the session-token is infinite (until next restart with \033[33m--ses-db\033[0m wiped)")
|
||||
ap2.add_argument("--idp-login", metavar="L", type=u, default="", help="replace all login-buttons with a link to URL \033[33mL\033[0m (unless \033[32mpw\033[0m is in \033[33m--auth-ord\033[0m then both will be shown); [\033[32m{dst}\033[0m] expands to url of current page")
|
||||
ap2.add_argument("--idp-login-t", metavar="T", type=u, default="Login with SSO", help="the label/text for the idp-login button")
|
||||
ap2.add_argument("--idp-logout", metavar="L", type=u, default="", help="replace all logout-buttons with a link to URL \033[33mL\033[0m")
|
||||
ap2.add_argument("--auth-ord", metavar="TXT", type=u, default="idp,ipu", help="controls auth precedence; examples: [\033[32mpw,idp,ipu\033[0m], [\033[32mipu,pw,idp\033[0m], see --help-auth-ord")
|
||||
ap2.add_argument("--no-bauth", action="store_true", help="disable basic-authentication support; do not accept passwords from the 'Authenticate' header at all. NOTE: This breaks support for the android app")
|
||||
ap2.add_argument("--bauth-last", action="store_true", help="keeps basic-authentication enabled, but only as a last-resort; if a cookie is also provided then the cookie wins")
|
||||
ap2.add_argument("--ses-db", metavar="PATH", type=u, default=ses_db, help="where to store the sessions database (if you run multiple copyparty instances, make sure they use different DBs)")
|
||||
|
@ -1256,6 +1345,10 @@ def add_auth(ap):
|
|||
ap2.add_argument("--ipr", metavar="CIDR=USR", type=u, action="append", help="\033[34mREPEATABLE:\033[0m username \033[33mUSR\033[0m can only connect from an IP matching one or more \033[33mCIDR\033[0m (comma-sep.); example: [\033[32m192.168.123.0/24,172.16.0.0/16=dave]")
|
||||
ap2.add_argument("--have-idp-hdrs", type=u, default="", help=argparse.SUPPRESS)
|
||||
ap2.add_argument("--have-ipu-or-ipr", type=u, default="", help=argparse.SUPPRESS)
|
||||
ap2.add_argument("--ao-idp-before-pw", type=u, default="", help=argparse.SUPPRESS)
|
||||
ap2.add_argument("--ao-h-before-hm", type=u, default="", help=argparse.SUPPRESS)
|
||||
ap2.add_argument("--ao-ipu-wins", type=u, default="", help=argparse.SUPPRESS)
|
||||
ap2.add_argument("--ao-have-pw", type=u, default="", help=argparse.SUPPRESS)
|
||||
|
||||
|
||||
def add_chpw(ap):
|
||||
|
@ -1287,6 +1380,8 @@ def add_zc_mdns(ap):
|
|||
ap2.add_argument("--zm6", action="store_true", help="IPv6 only")
|
||||
ap2.add_argument("--zmv", action="store_true", help="verbose mdns")
|
||||
ap2.add_argument("--zmvv", action="store_true", help="verboser mdns")
|
||||
ap2.add_argument("--zm-http", metavar="PORT", type=int, default=-1, help="port to announce for http/webdav; [\033[32m-1\033[0m] = auto, [\033[32m0\033[0m] = disabled, [\033[32m4649\033[0m] = port 4649")
|
||||
ap2.add_argument("--zm-https", metavar="PORT", type=int, default=-1, help="port to announce for https/webdavs; [\033[32m-1\033[0m] = auto, [\033[32m0\033[0m] = disabled, [\033[32m4649\033[0m] = port 4649")
|
||||
ap2.add_argument("--zm-no-pe", action="store_true", help="mute parser errors (invalid incoming MDNS packets)")
|
||||
ap2.add_argument("--zm-nwa-1", action="store_true", help="disable workaround for avahi-bug #379 (corruption in Avahi's mDNS reflection feature)")
|
||||
ap2.add_argument("--zms", metavar="dhf", type=u, default="", help="list of services to announce -- d=webdav h=http f=ftp s=smb -- lowercase=plaintext uppercase=TLS -- default: all enabled services except http/https (\033[32mDdfs\033[0m if \033[33m--ftp\033[0m and \033[33m--smb\033[0m is set, \033[32mDd\033[0m otherwise)")
|
||||
|
@ -1360,6 +1455,10 @@ def add_smb(ap):
|
|||
ap2.add_argument("--smbvv", action="store_true", help="verboser")
|
||||
ap2.add_argument("--smbvvv", action="store_true", help="verbosest")
|
||||
|
||||
def add_opds(ap):
|
||||
ap2 = ap.add_argument_group("OPDS options")
|
||||
ap2.add_argument("--opds", action="store_true", help="enable opds -- allows e-book readers to browse and download files (volflag=opds)")
|
||||
ap2.add_argument("--opds-exts", metavar="T,T", type=u, default="epub,cbz,pdf", help="file formats to list in OPDS feeds; leave empty to show everything (volflag=opds_exts)")
|
||||
|
||||
def add_handlers(ap):
|
||||
ap2 = ap.add_argument_group("handlers (see --help-handlers)")
|
||||
|
@ -1401,6 +1500,7 @@ def add_yolo(ap):
|
|||
ap2.add_argument("--no-fnugg", action="store_true", help="disable the smoketest for caching-related issues in the web-UI")
|
||||
ap2.add_argument("--getmod", action="store_true", help="permit ?move=[...] and ?delete as GET")
|
||||
ap2.add_argument("--wo-up-readme", action="store_true", help="allow users with write-only access to upload logues and readmes without adding the _wo_ filename prefix (volflag=wo_up_readme)")
|
||||
ap2.add_argument("--unsafe-state", action="store_true", help="when one of the emergency fallback locations are used for runtime state ($TMPDIR, /tmp), certain features will be force-disabled for security reasons by default. This option overrides that safeguard and allows unsafe storage of secrets")
|
||||
|
||||
|
||||
def add_optouts(ap):
|
||||
|
@ -1414,7 +1514,7 @@ def add_optouts(ap):
|
|||
ap2.add_argument("--no-fs-abrt", action="store_true", help="disable ability to abort ongoing copy/move")
|
||||
ap2.add_argument("-nth", action="store_true", help="no title hostname; don't show \033[33m--name\033[0m in <title>")
|
||||
ap2.add_argument("-nih", action="store_true", help="no info hostname -- don't show in UI")
|
||||
ap2.add_argument("-nid", action="store_true", help="no info disk-usage -- don't show in UI")
|
||||
ap2.add_argument("-nid", action="store_true", help="no info disk-usage -- don't show in UI. This is the same as --du-who no")
|
||||
ap2.add_argument("-nb", action="store_true", help="no powered-by-copyparty branding in UI")
|
||||
ap2.add_argument("--zipmaxn", metavar="N", type=u, default="0", help="reject download-as-zip if more than \033[33mN\033[0m files in total; optionally takes a unit suffix: [\033[32m256\033[0m], [\033[32m9K\033[0m], [\033[32m4G\033[0m] (volflag=zipmaxn)")
|
||||
ap2.add_argument("--zipmaxs", metavar="SZ", type=u, default="0", help="reject download-as-zip if total download size exceeds \033[33mSZ\033[0m bytes; optionally takes a unit suffix: [\033[32m256M\033[0m], [\033[32m4G\033[0m], [\033[32m2T\033[0m] (volflag=zipmaxs)")
|
||||
|
@ -1433,7 +1533,7 @@ def add_optouts(ap):
|
|||
def add_safety(ap):
|
||||
ap2 = ap.add_argument_group("safety options")
|
||||
ap2.add_argument("-s", action="count", default=0, help="increase safety: Disable thumbnails / potentially dangerous software (ffmpeg/pillow/vips), hide partial uploads, avoid crawlers.\n └─Alias of\033[32m --dotpart --no-thumb --no-mtag-ff --no-robots --force-js")
|
||||
ap2.add_argument("-ss", action="store_true", help="further increase safety: Prevent js-injection, accidental move/delete, broken symlinks, webdav requires login, 404 on 403, ban on excessive 404s.\n └─Alias of\033[32m -s --unpost=0 --no-del --no-mv --hardlink --dav-auth --vague-403 -nih")
|
||||
ap2.add_argument("-ss", action="store_true", help="further increase safety: Prevent js-injection, accidental move/delete, broken symlinks, webdav requires login, 404 on 403, ban on excessive 404s.\n └─Alias of\033[32m -s --unpost=0 --no-del --no-mv --reflink --dav-auth --vague-403 -nih")
|
||||
ap2.add_argument("-sss", action="store_true", help="further increase safety: Enable logging to disk, scan for dangerous symlinks.\n └─Alias of\033[32m -ss --no-dav --no-logues --no-readme -lo=cpp-%%Y-%%m%%d-%%H%%M%%S.txt.xz --ls=**,*,ln,p,r")
|
||||
ap2.add_argument("--ls", metavar="U[,V[,F]]", type=u, default="", help="do a sanity/safety check of all volumes on startup; arguments \033[33mUSER\033[0m,\033[33mVOL\033[0m,\033[33mFLAGS\033[0m (see \033[33m--help-ls\033[0m); example [\033[32m**,*,ln,p,r\033[0m]")
|
||||
ap2.add_argument("--xvol", action="store_true", help="never follow symlinks leaving the volume root, unless the link is into another volume where the user has similar access (volflag=xvol)")
|
||||
|
@ -1453,7 +1553,7 @@ def add_safety(ap):
|
|||
ap2.add_argument("--ban-422", metavar="N,W,B", type=u, default="9,2,1440", help="hitting more than \033[33mN\033[0m 422's in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes (invalid requests, attempted exploits ++)")
|
||||
ap2.add_argument("--ban-url", metavar="N,W,B", type=u, default="9,2,1440", help="hitting more than \033[33mN\033[0m sus URL's in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes; applies only to permissions g/G/h (decent replacement for \033[33m--ban-404\033[0m if that can't be used)")
|
||||
ap2.add_argument("--sus-urls", metavar="R", type=u, default=r"\.php$|(^|/)wp-(admin|content|includes)/", help="URLs which are considered sus / eligible for banning; disable with blank or [\033[32mno\033[0m]")
|
||||
ap2.add_argument("--nonsus-urls", metavar="R", type=u, default=r"^(favicon\.ico|robots\.txt)$|^apple-touch-icon|^\.well-known", help="harmless URLs ignored from 404-bans; disable with blank or [\033[32mno\033[0m]")
|
||||
ap2.add_argument("--nonsus-urls", metavar="R", type=u, default=r"^(favicon\..{3}|robots\.txt)$|^apple-touch-icon|^\.well-known", help="harmless URLs ignored from 403/404-bans; disable with blank or [\033[32mno\033[0m]")
|
||||
ap2.add_argument("--early-ban", action="store_true", help="if a client is banned, reject its connection as soon as possible; not a good idea to enable when proxied behind cloudflare since it could ban your reverse-proxy")
|
||||
ap2.add_argument("--cookie-nmax", metavar="N", type=int, default=50, help="reject HTTP-request from client if they send more than N cookies")
|
||||
ap2.add_argument("--cookie-cmax", metavar="N", type=int, default=8192, help="reject HTTP-request from client if more than N characters in Cookie header")
|
||||
|
@ -1495,11 +1595,15 @@ def add_logging(ap):
|
|||
ap2.add_argument("--log-utc", action="store_true", help="do not use local timezone; assume the TZ env-var is UTC (tiny bit faster)")
|
||||
ap2.add_argument("--log-tdec", metavar="N", type=int, default=3, help="timestamp resolution / number of timestamp decimals")
|
||||
ap2.add_argument("--log-badpwd", metavar="N", type=int, default=2, help="log failed login attempt passwords: 0=terse, 1=plaintext, 2=hashed")
|
||||
ap2.add_argument("--log-badxml", action="store_true", help="log any invalid XML received from a client")
|
||||
ap2.add_argument("--log-conn", action="store_true", help="debug: print tcp-server msgs")
|
||||
ap2.add_argument("--log-htp", action="store_true", help="debug: print http-server threadpool scaling")
|
||||
ap2.add_argument("--ihead", metavar="HEADER", type=u, action='append', help="print request \033[33mHEADER\033[0m; [\033[32m*\033[0m]=all")
|
||||
ap2.add_argument("--ohead", metavar="HEADER", type=u, action='append', help="print response \033[33mHEADER\033[0m; [\033[32m*\033[0m]=all")
|
||||
ap2.add_argument("--lf-url", metavar="RE", type=u, default=r"^/\.cpr/|[?&]th=[wjp]|/\.(_|ql_|DS_Store$|localized$)", help="dont log URLs matching regex \033[33mRE\033[0m")
|
||||
ap2.add_argument("--scan-st-r", metavar="SEC", type=float, default=0.1, help="fs-indexing: wait \033[33mSEC\033[0m between each status-message")
|
||||
ap2.add_argument("--scan-pr-r", metavar="SEC", type=float, default=10, help="fs-indexing: wait \033[33mSEC\033[0m between each 'progress:' message")
|
||||
ap2.add_argument("--scan-pr-s", metavar="MiB", type=float, default=1, help="fs-indexing: say 'file: <name>' when a file larger than \033[33mMiB\033[0m is about to be hashed")
|
||||
|
||||
|
||||
def add_admin(ap):
|
||||
|
@ -1607,6 +1711,7 @@ def add_db_general(ap, hcores):
|
|||
ap2.add_argument("--hash-mt", metavar="CORES", type=int, default=hcores, help="num cpu cores to use for file hashing; set 0 or 1 for single-core hashing")
|
||||
ap2.add_argument("--re-maxage", metavar="SEC", type=int, default=0, help="rescan filesystem for changes every \033[33mSEC\033[0m seconds; 0=off (volflag=scan)")
|
||||
ap2.add_argument("--db-act", metavar="SEC", type=float, default=10.0, help="defer any scheduled volume reindexing until \033[33mSEC\033[0m seconds after last db write (uploads, renames, ...)")
|
||||
ap2.add_argument("--srch-icase", action="store_true", help="case-insensitive search for all unicode characters (the default is icase for just ascii). NOTE: will make searches much slower (around 4x), and NOTE: only applies to filenames/paths, not tags")
|
||||
ap2.add_argument("--srch-time", metavar="SEC", type=int, default=45, help="search deadline -- terminate searches running for more than \033[33mSEC\033[0m seconds")
|
||||
ap2.add_argument("--srch-hits", metavar="N", type=int, default=7999, help="max search results to allow clients to fetch; 125 results will be shown initially")
|
||||
ap2.add_argument("--srch-excl", metavar="PTN", type=u, default="", help="regex: exclude files from search results if the file-URL matches \033[33mPTN\033[0m (case-sensitive). Example: [\033[32mpassword|logs/[0-9]\033[0m] any URL containing 'password' or 'logs/DIGIT' (volflag=srch_excl)")
|
||||
|
@ -1632,6 +1737,7 @@ def add_db_metadata(ap):
|
|||
|
||||
def add_txt(ap):
|
||||
ap2 = ap.add_argument_group("textfile options")
|
||||
ap2.add_argument("--md-no-br", action="store_true", help="markdown: disable newline-is-newline; will only render a newline into the html given two trailing spaces or a double-newline (volflag=md_no_br)")
|
||||
ap2.add_argument("--md-hist", metavar="TXT", type=u, default="s", help="where to store old version of markdown files; [\033[32ms\033[0m]=subfolder, [\033[32mv\033[0m]=volume-histpath, [\033[32mn\033[0m]=nope/disabled (volflag=md_hist)")
|
||||
ap2.add_argument("--txt-eol", metavar="TYPE", type=u, default="", help="enable EOL conversion when writing documents; supported: CRLF, LF (volflag=txt_eol)")
|
||||
ap2.add_argument("-mcr", metavar="SEC", type=int, default=60, help="the textfile editor will check for serverside changes every \033[33mSEC\033[0m seconds")
|
||||
|
@ -1660,12 +1766,13 @@ def add_og(ap):
|
|||
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):
|
||||
def add_ui(ap, retry: int):
|
||||
THEMES = 10
|
||||
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("--gsel", action="store_true", help="select files in grid by ctrl-click (volflag=gsel)")
|
||||
ap2.add_argument("--localtime", action="store_true", help="default to local timezone instead of UTC")
|
||||
ap2.add_argument("--ui-filesz", metavar="FMT", type=u, default="1", help="default filesize format; one of these: 0, 1, 2, 2c, 3, 3c, 4, 4c, 5, 5c, fuzzy (see UI)")
|
||||
ap2.add_argument("--lang", metavar="LANG", type=u, default="eng", help="language, for example \033[32meng\033[0m / \033[32mnor\033[0m / ...")
|
||||
ap2.add_argument("--theme", metavar="NUM", type=int, default=0, help="default theme to use (0..%d)" % (THEMES - 1,))
|
||||
ap2.add_argument("--themes", metavar="NUM", type=int, default=THEMES, help="number of themes installed")
|
||||
|
@ -1677,6 +1784,7 @@ def add_ui(ap, retry):
|
|||
ap2.add_argument("--qdel", metavar="LVL", type=int, default=2, help="number of confirmations to show when deleting files (2/1/0)")
|
||||
ap2.add_argument("--unlist", metavar="REGEX", type=u, default="", help="don't show files/folders matching \033[33mREGEX\033[0m in file list. WARNING: 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("--ufavico", metavar="TXT", type=u, default="", help="URL to .ico/png/gif/svg file; \033[33m--favico\033[0m takes precedence unless disabled (volflag=ufavico)")
|
||||
ap2.add_argument("--ext-th", metavar="E=VP", type=u, action="append", help="\033[34mREPEATABLE:\033[0m use thumbnail-image \033[33mVP\033[0m for file-extension \033[33mE\033[0m, example: [\033[32mexe=/.res/exe.png\033[0m] (volflag=ext_th)")
|
||||
ap2.add_argument("--mpmc", type=u, default="", help=argparse.SUPPRESS)
|
||||
ap2.add_argument("--spinner", metavar="TXT", type=u, default="🌲", help="\033[33memoji\033[0m or \033[33memoji,css\033[0m Example: [\033[32m🥖,padding:0\033[0m]")
|
||||
|
@ -1684,13 +1792,18 @@ def add_ui(ap, retry):
|
|||
ap2.add_argument("--js-browser", metavar="L", type=u, default="", help="URL to additional JS to include in the filebrowser html")
|
||||
ap2.add_argument("--js-other", metavar="L", type=u, default="", help="URL to additional JS to include in all other pages")
|
||||
ap2.add_argument("--html-head", metavar="TXT", type=u, default="", help="text to append to the <head> of all HTML pages (except for basic-browser); 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("--html-head-s", metavar="T", type=u, default="", help="text to append to the <head> of all HTML pages (except for basic-browser); similar to (and can be combined with) --html-head but only accepts static text (volflag=html_head_s)")
|
||||
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)")
|
||||
ap2.add_argument("--doctitle", metavar="TXT", type=u, default="copyparty @ --name", help="title / service-name to show in html documents")
|
||||
ap2.add_argument("--bname", metavar="TXT", type=u, default="--name", help="server name (displayed in filebrowser document title)")
|
||||
ap2.add_argument("--pb-url", metavar="URL", type=u, default=URL_PRJ, help="powered-by link; disable with \033[33m-nb\033[0m")
|
||||
ap2.add_argument("--ver", action="store_true", help="show version on the control panel (incompatible with \033[33m-nb\033[0m)")
|
||||
ap2.add_argument("--ver", action="store_true", help="show version on the control panel (incompatible with \033[33m-nb\033[0m). This is the same as --ver-who all")
|
||||
ap2.add_argument("--ver-who", metavar="TXT", type=u, default="no", help="only show version for: [\033[32ma\033[0m]=admin-permission-anywhere, [\033[32mauth\033[0m]=authenticated, [\033[32mall\033[0m]=anyone")
|
||||
ap2.add_argument("--du-who", metavar="TXT", type=u, default="all", help="only show disk usage for: [\033[32mno\033[0m]=nobody, [\033[32ma\033[0m]=admin-permission, [\033[32mrw\033[0m]=read-write, [\033[32mw\033[0m]=write, [\033[32mauth\033[0m]=authenticated, [\033[32mall\033[0m]=anyone (volflag=du_who)")
|
||||
ap2.add_argument("--ver-iwho", type=int, default=0, help=argparse.SUPPRESS)
|
||||
ap2.add_argument("--du-iwho", type=int, default=0, help=argparse.SUPPRESS)
|
||||
ap2.add_argument("--k304", metavar="NUM", type=int, default=0, help="configure the option to enable/disable k304 on the controlpanel (workaround for buggy reverse-proxies); [\033[32m0\033[0m] = hidden and default-off, [\033[32m1\033[0m] = visible and default-off, [\033[32m2\033[0m] = visible and default-on")
|
||||
ap2.add_argument("--no304", metavar="NUM", type=int, default=0, help="configure the option to enable/disable no304 on the controlpanel (workaround for buggy caching in browsers); [\033[32m0\033[0m] = hidden and default-off, [\033[32m1\033[0m] = visible and default-off, [\033[32m2\033[0m] = visible and default-on")
|
||||
ap2.add_argument("--ctl-re", metavar="SEC", type=int, default=1, help="the controlpanel Refresh-button will autorefresh every SEC; [\033[32m0\033[0m] = just once")
|
||||
|
@ -1731,7 +1844,7 @@ def add_debug(ap):
|
|||
|
||||
|
||||
def run_argparse(
|
||||
argv: list[str], formatter: Any, retry: bool, nc: int, verbose=True
|
||||
argv: list[str], formatter: Any, retry: int, nc: int, verbose=True
|
||||
) -> argparse.Namespace:
|
||||
ap = argparse.ArgumentParser(
|
||||
formatter_class=formatter,
|
||||
|
@ -1777,6 +1890,7 @@ def run_argparse(
|
|||
add_webdav(ap)
|
||||
add_tftp(ap)
|
||||
add_smb(ap)
|
||||
add_opds(ap)
|
||||
add_safety(ap)
|
||||
add_salt(ap, fk_salt, dk_salt, ah_salt)
|
||||
add_optouts(ap)
|
||||
|
@ -1798,18 +1912,21 @@ def run_argparse(
|
|||
for k, h, _ in sects:
|
||||
ap2.add_argument("--help-" + k, action="store_true", help=h)
|
||||
|
||||
try:
|
||||
if not retry:
|
||||
raise Exception()
|
||||
|
||||
if retry:
|
||||
a = ["ascii", "replace"]
|
||||
for x in ap._actions:
|
||||
if not x.help:
|
||||
continue
|
||||
try:
|
||||
x.default = x.default.encode(*a).decode(*a)
|
||||
except:
|
||||
pass
|
||||
|
||||
a = ["ascii", "replace"]
|
||||
x.help = x.help.encode(*a).decode(*a) + "\033[0m"
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
if x.help and x.help is not argparse.SUPPRESS:
|
||||
x.help = x.help.replace("└─", "`-").encode(*a).decode(*a)
|
||||
if retry > 2:
|
||||
x.help = RE_ANSI.sub("", x.help)
|
||||
except:
|
||||
pass
|
||||
|
||||
ret = ap.parse_args(args=argv[1:])
|
||||
for k, h, t in sects:
|
||||
|
@ -1919,7 +2036,7 @@ def main(argv: Optional[list[str]] = None) -> None:
|
|||
except:
|
||||
nc = 486 # mdns/ssdp restart headroom; select() maxfd is 512 on windows
|
||||
|
||||
retry = False
|
||||
retry = 0
|
||||
for fmtr in [RiceFormatter, RiceFormatter, Dodge11874, BasicDodge11874]:
|
||||
try:
|
||||
al = run_argparse(argv, fmtr, retry, nc)
|
||||
|
@ -1928,8 +2045,9 @@ def main(argv: Optional[list[str]] = None) -> None:
|
|||
except SystemExit:
|
||||
raise
|
||||
except:
|
||||
retry = True
|
||||
lprint("\n[ {} ]:\n{}\n".format(fmtr, min_ex()))
|
||||
retry += 1
|
||||
t = "WARNING: due to limitations in your terminal and/or OS, the helptext cannot be displayed correctly. Will show a simplified version due to the following error:\n[ %s ]:\n%s\n"
|
||||
lprint(t % (fmtr, min_ex()))
|
||||
|
||||
try:
|
||||
assert al # type: ignore
|
||||
|
@ -1938,6 +2056,9 @@ def main(argv: Optional[list[str]] = None) -> None:
|
|||
except:
|
||||
sys.exit(1)
|
||||
|
||||
if al.chdir:
|
||||
os.chdir(al.chdir)
|
||||
|
||||
if al.ansi:
|
||||
al.no_ansi = False
|
||||
elif not al.no_ansi:
|
||||
|
@ -1965,7 +2086,7 @@ def main(argv: Optional[list[str]] = None) -> None:
|
|||
if not HAVE_IPV6 and al.i == "::":
|
||||
al.i = "0.0.0.0"
|
||||
|
||||
al.i = al.i.split(",")
|
||||
al.i = [x.strip() for x in al.i.split(",")]
|
||||
try:
|
||||
if "-" in al.p:
|
||||
lo, hi = [int(x) for x in al.p.split("-")]
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
# coding: utf-8
|
||||
|
||||
VERSION = (1, 19, 3)
|
||||
VERSION = (1, 19, 16)
|
||||
CODENAME = "usernames"
|
||||
BUILD_DT = (2025, 8, 17)
|
||||
BUILD_DT = (2025, 10, 5)
|
||||
|
||||
S_VERSION = ".".join(map(str, VERSION))
|
||||
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)
|
||||
|
|
|
@ -13,7 +13,7 @@ import threading
|
|||
import time
|
||||
from datetime import datetime
|
||||
|
||||
from .__init__ import ANYWIN, PY2, TYPE_CHECKING, WINDOWS, E
|
||||
from .__init__ import ANYWIN, MACOS, PY2, TYPE_CHECKING, WINDOWS, E
|
||||
from .bos import bos
|
||||
from .cfg import flagdescs, permdescs, vf_bmap, vf_cmap, vf_vmap
|
||||
from .pwhash import PWHash
|
||||
|
@ -21,8 +21,10 @@ from .util import (
|
|||
DEF_MTE,
|
||||
DEF_MTH,
|
||||
EXTS,
|
||||
FAVICON_MIMES,
|
||||
HAVE_SQLITE3,
|
||||
IMPLICATIONS,
|
||||
META_NOBOTS,
|
||||
MIMES,
|
||||
SQLITE_VER,
|
||||
UNPLICATIONS,
|
||||
|
@ -99,6 +101,8 @@ SBADCFG = " ({})".format(BAD_CFG)
|
|||
|
||||
PTN_U_GRP = re.compile(r"\$\{u(%[+-][^}]+)\}")
|
||||
PTN_G_GRP = re.compile(r"\$\{g(%[+-][^}]+)\}")
|
||||
PTN_U_ANY = re.compile(r"(\${[u][}%])")
|
||||
PTN_G_ANY = re.compile(r"(\${[g][}%])")
|
||||
PTN_SIGIL = re.compile(r"(\${[ug][}%])")
|
||||
|
||||
|
||||
|
@ -167,14 +171,19 @@ class Lim(object):
|
|||
self.rotn = 0 # rot num files
|
||||
self.rotl = 0 # rot depth
|
||||
self.rotf = "" # rot datefmt
|
||||
self.rotf_tz = UTC # rot timezone
|
||||
self.rot_re = re.compile("") # rotf check
|
||||
|
||||
def log(self, msg: str, c: Union[int, str] = 0) -> None:
|
||||
if self.log_func:
|
||||
self.log_func("up-lim", msg, c)
|
||||
|
||||
def set_rotf(self, fmt: str) -> None:
|
||||
def set_rotf(self, fmt: str, tz: str) -> None:
|
||||
self.rotf = fmt
|
||||
if tz != "UTC":
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
self.rotf_tz = ZoneInfo(tz)
|
||||
r = re.escape(fmt).replace("%Y", "[0-9]{4}").replace("%j", "[0-9]{3}")
|
||||
r = re.sub("%[mdHMSWU]", "[0-9]{2}", r)
|
||||
self.rot_re = re.compile("(^|/)" + r + "$")
|
||||
|
@ -278,7 +287,7 @@ class Lim(object):
|
|||
if self.rot_re.search(path.replace("\\", "/")):
|
||||
return path, ""
|
||||
|
||||
suf = datetime.now(UTC).strftime(self.rotf)
|
||||
suf = datetime.now(self.rotf_tz).strftime(self.rotf)
|
||||
if path:
|
||||
path += "/"
|
||||
|
||||
|
@ -424,10 +433,14 @@ class VFS(object):
|
|||
self.all_nodes[vpath] = self
|
||||
self.all_aps = [(rp, [self])]
|
||||
self.all_vps = [(vp, self)]
|
||||
self.canonical = self._canonical
|
||||
self.dcanonical = self._dcanonical
|
||||
else:
|
||||
self.histpath = self.dbpath = ""
|
||||
self.all_aps = []
|
||||
self.all_vps = []
|
||||
self.canonical = self._canonical_null
|
||||
self.dcanonical = self._dcanonical_null
|
||||
|
||||
self.get_dbv = self._get_dbv
|
||||
self.ls = self._ls
|
||||
|
@ -624,7 +637,39 @@ class VFS(object):
|
|||
vrem = vjoin(self.vpath[len(dbv.vpath) :].lstrip("/"), vrem)
|
||||
return dbv, vrem
|
||||
|
||||
def canonical(self, rem: str, resolve: bool = True) -> str:
|
||||
def casechk(self, rem: str, do_stat: bool) -> bool:
|
||||
ap = self.canonical(rem, False)
|
||||
if do_stat and not bos.path.exists(ap):
|
||||
return True # doesn't exist at all; good to go
|
||||
dp, fn = os.path.split(ap)
|
||||
if not fn:
|
||||
return True # filesystem root
|
||||
try:
|
||||
fns = os.listdir(dp)
|
||||
except:
|
||||
return True # maybe chmod 111; assume ok
|
||||
if fn in fns:
|
||||
return True
|
||||
hit = "<?>"
|
||||
lfn = fn.lower()
|
||||
for zs in fns:
|
||||
if lfn == zs.lower():
|
||||
hit = zs
|
||||
break
|
||||
if not hit:
|
||||
return True # NFC/NFD or something, can't be helped either way
|
||||
if self.log:
|
||||
t = "returning 404 due to underlying case-insensitive filesystem:\n http-req: %r\n local-fs: %r"
|
||||
self.log("vfs", t % (fn, hit))
|
||||
return False
|
||||
|
||||
def _canonical_null(self, rem: str, resolve: bool = True) -> str:
|
||||
return ""
|
||||
|
||||
def _dcanonical_null(self, rem: str) -> str:
|
||||
return ""
|
||||
|
||||
def _canonical(self, rem: str, resolve: bool = True) -> str:
|
||||
"""returns the canonical path (fully-resolved absolute fs path)"""
|
||||
ap = self.realpath
|
||||
if rem:
|
||||
|
@ -632,7 +677,7 @@ class VFS(object):
|
|||
|
||||
return absreal(ap) if resolve else ap
|
||||
|
||||
def dcanonical(self, rem: str) -> str:
|
||||
def _dcanonical(self, rem: str) -> str:
|
||||
"""resolves until the final component (filename)"""
|
||||
ap = self.realpath
|
||||
if rem:
|
||||
|
@ -641,6 +686,44 @@ class VFS(object):
|
|||
ad, fn = os.path.split(ap)
|
||||
return os.path.join(absreal(ad), fn)
|
||||
|
||||
def _canonical_shr(self, rem: str, resolve: bool = True) -> str:
|
||||
"""returns the canonical path (fully-resolved absolute fs path)"""
|
||||
ap = self.realpath
|
||||
if rem:
|
||||
ap += "/" + rem
|
||||
|
||||
rap = absreal(ap)
|
||||
if self.shr_files:
|
||||
assert self.shr_src # !rm
|
||||
vn, rem = self.shr_src
|
||||
chk = absreal(os.path.join(vn.realpath, rem))
|
||||
if chk != rap:
|
||||
# not the dir itself; assert file allowed
|
||||
ad, fn = os.path.split(rap)
|
||||
if chk != ad or fn not in self.shr_files:
|
||||
return "\n\n"
|
||||
|
||||
return rap if resolve else ap
|
||||
|
||||
def _dcanonical_shr(self, rem: str) -> str:
|
||||
"""resolves until the final component (filename)"""
|
||||
ap = self.realpath
|
||||
if rem:
|
||||
ap += "/" + rem
|
||||
|
||||
ad, fn = os.path.split(ap)
|
||||
ad = absreal(ad)
|
||||
if self.shr_files:
|
||||
assert self.shr_src # !rm
|
||||
vn, rem = self.shr_src
|
||||
chk = absreal(os.path.join(vn.realpath, rem))
|
||||
if chk != absreal(ap):
|
||||
# not the dir itself; assert file allowed
|
||||
if ad != chk or fn not in self.shr_files:
|
||||
return "\n\n"
|
||||
|
||||
return os.path.join(ad, fn)
|
||||
|
||||
def _ls_nope(
|
||||
self, *a, **ka
|
||||
) -> tuple[str, list[tuple[str, os.stat_result]], dict[str, "VFS"]]:
|
||||
|
@ -673,8 +756,12 @@ class VFS(object):
|
|||
"""return user-readable [fsdir,real,virt] items at vpath"""
|
||||
virt_vis = {} # nodes readable by user
|
||||
abspath = self.canonical(rem)
|
||||
real = list(statdir(self.log, scandir, lstat, abspath, throw))
|
||||
real.sort()
|
||||
if abspath:
|
||||
real = list(statdir(self.log, scandir, lstat, abspath, throw))
|
||||
real.sort()
|
||||
else:
|
||||
real = []
|
||||
|
||||
if not rem:
|
||||
# no vfs nodes in the list of real inodes
|
||||
real = [x for x in real if x[0] not in self.nodes]
|
||||
|
@ -976,6 +1063,14 @@ class AuthSrv(object):
|
|||
self.indent = ""
|
||||
self.is_lxc = args.c == ["/z/initcfg"]
|
||||
|
||||
self._vf0b = {
|
||||
"tcolor": self.args.tcolor,
|
||||
"du_iwho": self.args.du_iwho,
|
||||
"shr_who": self.args.shr_who if self.args.shr else "no",
|
||||
}
|
||||
self._vf0 = self._vf0b.copy()
|
||||
self._vf0["d2d"] = True
|
||||
|
||||
# fwd-decl
|
||||
self.vfs = VFS(log_func, "", "", "", AXS(), {})
|
||||
self.acct: dict[str, str] = {} # uname->pw
|
||||
|
@ -1014,7 +1109,10 @@ class AuthSrv(object):
|
|||
yield prev, True
|
||||
|
||||
def vf0(self):
|
||||
return {"d2d": True, "tcolor": self.args.tcolor}
|
||||
return self._vf0.copy()
|
||||
|
||||
def vf0b(self):
|
||||
return self._vf0b.copy()
|
||||
|
||||
def idp_checkin(
|
||||
self, broker: Optional["BrokerCli"], uname: str, gname: str
|
||||
|
@ -1080,6 +1178,16 @@ class AuthSrv(object):
|
|||
src0 = src # abspath
|
||||
dst0 = dst # vpath
|
||||
|
||||
zsl = []
|
||||
for ptn, sigil in ((PTN_U_ANY, "${u}"), (PTN_G_ANY, "${g}")):
|
||||
if bool(ptn.search(src)) != bool(ptn.search(dst)):
|
||||
zsl.append(sigil)
|
||||
if zsl:
|
||||
t = "ERROR: if %s is mentioned in a volume definition, it must be included in both the filesystem-path [%s] and the volume-url [/%s]"
|
||||
t = "\n".join([t % (x, src, dst) for x in zsl])
|
||||
self.log(t, 1)
|
||||
raise Exception(t)
|
||||
|
||||
un_gn = [(un, gn) for un, gns in un_gns.items() for gn in gns]
|
||||
if not un_gn:
|
||||
# ensure volume creation if there's no users
|
||||
|
@ -1172,8 +1280,8 @@ class AuthSrv(object):
|
|||
self.log(t, c=3)
|
||||
raise Exception(BAD_CFG)
|
||||
|
||||
if not bos.path.isdir(src):
|
||||
self.log("warning: filesystem-path does not exist: {}".format(src), 3)
|
||||
if not bos.path.exists(src):
|
||||
self.log("warning: filesystem-path did not exist: %r" % (src,), 3)
|
||||
|
||||
mount[dst] = (src, dst0)
|
||||
daxs[dst] = AXS()
|
||||
|
@ -1328,6 +1436,10 @@ class AuthSrv(object):
|
|||
zt = split_cfg_ln(ln)
|
||||
for zs, za in zt.items():
|
||||
zs = zs.lstrip("-")
|
||||
if "=" in zs:
|
||||
t = "WARNING: found an option named [%s] in your [global] config; did you mean to say [%s: %s] instead?"
|
||||
zs1, zs2 = zs.split("=", 1)
|
||||
self.log(t % (zs, zs1, zs2), 3)
|
||||
if za is True:
|
||||
self._e("└─argument [{}]".format(zs))
|
||||
else:
|
||||
|
@ -1337,6 +1449,10 @@ class AuthSrv(object):
|
|||
if cat == cata:
|
||||
try:
|
||||
u, p = [zs.strip() for zs in ln.split(":", 1)]
|
||||
if "=" in u and not p:
|
||||
t = "WARNING: found username [%s] in your [accounts] config; did you mean to say [%s: %s] instead?"
|
||||
zs1, zs2 = u.split("=", 1)
|
||||
self.log(t % (u, zs1, zs2), 3)
|
||||
self._l(ln, 5, "account [{}], password [{}]".format(u, p))
|
||||
acct[u] = p
|
||||
except:
|
||||
|
@ -1407,6 +1523,10 @@ class AuthSrv(object):
|
|||
zd = split_cfg_ln(ln)
|
||||
fstr = ""
|
||||
for sk, sv in zd.items():
|
||||
if "=" in sk:
|
||||
t = "WARNING: found a volflag named [%s] in your config; did you mean to say [%s: %s] instead?"
|
||||
zs1, zs2 = sk.split("=", 1)
|
||||
self.log(t % (sk, zs1, zs2), 3)
|
||||
bad = re.sub(r"[a-z0-9_-]", "", sk).lstrip("-")
|
||||
if bad:
|
||||
err = "bad characters [{}] in volflag name [{}]; "
|
||||
|
@ -1647,6 +1767,7 @@ class AuthSrv(object):
|
|||
# accept both , and : as separators between usernames
|
||||
zs1, zs2 = x.replace("=", ":").split(":", 1)
|
||||
grps[zs1] = zs2.replace(":", ",").split(",")
|
||||
grps[zs1] = [x.strip() for x in grps[zs1]]
|
||||
except:
|
||||
t = '\n invalid value "{}" for argument --grp, must be groupname:username1,username2,...'
|
||||
raise Exception(t.format(x))
|
||||
|
@ -1698,8 +1819,8 @@ class AuthSrv(object):
|
|||
self.log("\n{0}\n{1}{0}".format(t, "\n".join(slns)))
|
||||
raise
|
||||
|
||||
self.args.have_idp_hdrs = bool(self.args.idp_h_usr or self.args.idp_hm_usr)
|
||||
self.args.have_ipu_or_ipr = bool(self.args.ipu or self.args.ipr)
|
||||
derive_args(self.args)
|
||||
self.setup_auth_ord()
|
||||
|
||||
self.setup_pwhash(acct)
|
||||
defpw = acct.copy()
|
||||
|
@ -1737,12 +1858,15 @@ class AuthSrv(object):
|
|||
files = os.listdir(E.cfg)
|
||||
except:
|
||||
files = []
|
||||
hits = [x for x in files if x.lower().endswith(".conf")]
|
||||
hits = [
|
||||
x
|
||||
for x in files
|
||||
if x.lower().endswith(".conf") and not x.startswith(".")
|
||||
]
|
||||
if hits:
|
||||
t = "Hint: Found some config files in [%s], but these were not automatically loaded because they are in the wrong place%s %s\n"
|
||||
self.log(t % (E.cfg, ehint, ", ".join(hits)), 3)
|
||||
zvf = {"tcolor": self.args.tcolor}
|
||||
vfs = VFS(self.log_func, absreal("."), "", "", axs, zvf)
|
||||
vfs = VFS(self.log_func, absreal("."), "", "", axs, self.vf0b())
|
||||
if not axs.uread:
|
||||
self.badcfg1 = True
|
||||
elif "" not in mount:
|
||||
|
@ -1778,7 +1902,7 @@ class AuthSrv(object):
|
|||
vol.all_vps.sort(key=lambda x: len(x[0]), reverse=True)
|
||||
vol.root = vfs
|
||||
|
||||
zs = "neversymlink"
|
||||
zs = "neversymlink du_iwho"
|
||||
k_ign = set(zs.split())
|
||||
for vol in vfs.all_vols.values():
|
||||
unknown_flags = set()
|
||||
|
@ -1930,6 +2054,8 @@ class AuthSrv(object):
|
|||
promote = []
|
||||
demote = []
|
||||
for vol in vfs.all_vols.values():
|
||||
if not vol.realpath:
|
||||
continue
|
||||
hid = self.hid_cache.get(vol.realpath)
|
||||
if not hid:
|
||||
zb = hashlib.sha512(afsenc(vol.realpath)).digest()
|
||||
|
@ -1968,6 +2094,8 @@ class AuthSrv(object):
|
|||
vol.histpath = absreal(vol.histpath)
|
||||
|
||||
for vol in vfs.all_vols.values():
|
||||
if not vol.realpath:
|
||||
continue
|
||||
hid = self.hid_cache[vol.realpath]
|
||||
vflag = vol.flags.get("dbpath")
|
||||
if vflag == "-":
|
||||
|
@ -2096,7 +2224,7 @@ class AuthSrv(object):
|
|||
zs = vol.flags.get("rotf")
|
||||
if zs:
|
||||
use = True
|
||||
lim.set_rotf(zs)
|
||||
lim.set_rotf(zs, vol.flags.get("rotf_tz") or "UTC")
|
||||
|
||||
zs = vol.flags.get("maxn")
|
||||
if zs:
|
||||
|
@ -2276,6 +2404,11 @@ class AuthSrv(object):
|
|||
vol.lim.uid = vol.flags["uid"]
|
||||
vol.lim.gid = vol.flags["gid"]
|
||||
|
||||
vol.flags["du_iwho"] = n_du_who(vol.flags["du_who"])
|
||||
|
||||
if not enshare:
|
||||
vol.flags["shr_who"] = self.args.shr_who = "no"
|
||||
|
||||
if vol.flags.get("og"):
|
||||
self.args.uqe = True
|
||||
|
||||
|
@ -2362,6 +2495,45 @@ class AuthSrv(object):
|
|||
t = "WARNING: volume [/%s]: invalid value specified for ext-th: %s"
|
||||
self.log(t % (vol.vpath, etv), 3)
|
||||
|
||||
zs = str(vol.flags.get("html_head") or "")
|
||||
if zs and zs[:1] in "%@":
|
||||
vol.flags["html_head_d"] = zs
|
||||
head_s = str(vol.flags.get("html_head_s") or "")
|
||||
else:
|
||||
zs2 = str(vol.flags.get("html_head_s") or "")
|
||||
if zs2 and zs:
|
||||
head_s = "%s\n%s\n" % (zs2.strip(), zs.strip())
|
||||
else:
|
||||
head_s = zs2 or zs
|
||||
|
||||
if head_s and not head_s.endswith("\n"):
|
||||
head_s += "\n"
|
||||
|
||||
if "norobots" in vol.flags:
|
||||
head_s += META_NOBOTS
|
||||
|
||||
ico_url = vol.flags.get("ufavico")
|
||||
if ico_url:
|
||||
ico_h = ""
|
||||
ico_ext = ico_url.split("?")[0].split(".")[-1].lower()
|
||||
if ico_ext in FAVICON_MIMES:
|
||||
zs = '<link rel="icon" type="%s" href="%s">\n'
|
||||
ico_h = zs % (FAVICON_MIMES[ico_ext], ico_url)
|
||||
elif ico_ext == "ico":
|
||||
zs = '<link rel="shortcut icon" href="%s">\n'
|
||||
ico_h = zs % (ico_url,)
|
||||
if ico_h:
|
||||
vol.flags["ufavico_h"] = ico_h
|
||||
head_s += ico_h
|
||||
|
||||
if head_s:
|
||||
vol.flags["html_head_s"] = head_s
|
||||
else:
|
||||
vol.flags.pop("html_head_s", None)
|
||||
|
||||
if not vol.flags.get("html_head_d"):
|
||||
vol.flags.pop("html_head_d", None)
|
||||
|
||||
vol.check_landmarks()
|
||||
|
||||
# d2d drops all database features for a volume
|
||||
|
@ -2451,6 +2623,47 @@ class AuthSrv(object):
|
|||
self.log(t.format(vol.vpath, mtp), 1)
|
||||
errors = True
|
||||
|
||||
for vol in vfs.all_nodes.values():
|
||||
if not vol.realpath or os.path.isfile(vol.realpath):
|
||||
continue
|
||||
ccs = vol.flags["casechk"][:1].lower()
|
||||
if ccs in ("y", "n"):
|
||||
if ccs == "y":
|
||||
vol.flags["bcasechk"] = True
|
||||
continue
|
||||
try:
|
||||
bos.makedirs(vol.realpath, vf=vol.flags)
|
||||
files = os.listdir(vol.realpath)
|
||||
for fn in files:
|
||||
fn2 = fn.lower()
|
||||
if fn == fn2:
|
||||
fn2 = fn.upper()
|
||||
if fn == fn2 or fn2 in files:
|
||||
continue
|
||||
is_ci = os.path.exists(os.path.join(vol.realpath, fn2))
|
||||
ccs = "y" if is_ci else "n"
|
||||
break
|
||||
if ccs not in ("y", "n"):
|
||||
ap = os.path.join(vol.realpath, "casechk")
|
||||
open(ap, "wb").close()
|
||||
ccs = "y" if os.path.exists(ap[:-1] + "K") else "n"
|
||||
os.unlink(ap)
|
||||
except Exception as ex:
|
||||
if ANYWIN:
|
||||
zs = "Windows"
|
||||
ccs = "y"
|
||||
elif MACOS:
|
||||
zs = "Macos"
|
||||
ccs = "y"
|
||||
else:
|
||||
zs = "Linux"
|
||||
ccs = "n"
|
||||
t = "unable to determine if filesystem at %r is case-insensitive due to %r; assuming casechk=%s due to %s"
|
||||
self.log(t % (vol.realpath, ex, ccs, zs), 3)
|
||||
vol.flags["casechk"] = ccs
|
||||
if ccs == "y":
|
||||
vol.flags["bcasechk"] = True
|
||||
|
||||
tags = self.args.mtp or []
|
||||
tags = [x.split("=")[0] for x in tags]
|
||||
tags = [y for x in tags for y in x.split(",")]
|
||||
|
@ -2554,7 +2767,11 @@ class AuthSrv(object):
|
|||
|
||||
if "dedup" in zv.flags:
|
||||
have_dedup = True
|
||||
if "e2d" not in zv.flags and "hardlink" not in zv.flags:
|
||||
if (
|
||||
"e2d" not in zv.flags
|
||||
and "hardlink" not in zv.flags
|
||||
and "reflink" not in zv.flags
|
||||
):
|
||||
unsafe_dedup.append("/" + zv.vpath)
|
||||
|
||||
t += "\n"
|
||||
|
@ -2718,13 +2935,22 @@ class AuthSrv(object):
|
|||
|
||||
shn.shr_files = set(fns)
|
||||
shn.ls = shn._ls_shr
|
||||
shn.canonical = shn._canonical_shr
|
||||
shn.dcanonical = shn._dcanonical_shr
|
||||
else:
|
||||
shn.ls = shn._ls
|
||||
shn.canonical = shn._canonical
|
||||
shn.dcanonical = shn._dcanonical
|
||||
|
||||
shn.shr_owner = s_un
|
||||
shn.shr_src = (s_vfs, s_rem)
|
||||
shn.realpath = s_vfs.canonical(s_rem)
|
||||
|
||||
o_vn, _ = shn._get_share_src("")
|
||||
shn.flags = o_vn.flags.copy()
|
||||
shn.dbpath = o_vn.dbpath
|
||||
shn.histpath = o_vn.histpath
|
||||
|
||||
# root.all_aps doesn't include any shares, so make a copy where the
|
||||
# share appears in all abspaths it can provide (for example for chk_ap)
|
||||
ap = shn.realpath
|
||||
|
@ -2782,24 +3008,30 @@ class AuthSrv(object):
|
|||
"dcrop": vf["crop"],
|
||||
"dth3x": vf["th3x"],
|
||||
"u2ts": vf["u2ts"],
|
||||
"shr_who": vf["shr_who"],
|
||||
"frand": bool(vf.get("rand")),
|
||||
"lifetime": vf.get("lifetime") or 0,
|
||||
"unlist": vf.get("unlist") or "",
|
||||
"sb_lg": "" if "no_sb_lg" in vf else (vf.get("lg_sbf") or "y"),
|
||||
}
|
||||
if "ufavico_h" in vf:
|
||||
vn.js_ls["ufavico"] = vf["ufavico_h"]
|
||||
js_htm = {
|
||||
"SPINNER": self.args.spinner,
|
||||
"s_name": self.args.bname,
|
||||
"idp_login": self.args.idp_login,
|
||||
"have_up2k_idx": "e2d" in vf,
|
||||
"have_acode": not self.args.no_acode,
|
||||
"have_c2flac": self.args.allow_flac,
|
||||
"have_c2wav": self.args.allow_wav,
|
||||
"have_shr": self.args.shr,
|
||||
"shr_who": vf["shr_who"],
|
||||
"have_zip": not self.args.no_zip,
|
||||
"have_mv": not self.args.no_mv,
|
||||
"have_del": not self.args.no_del,
|
||||
"have_unpost": int(self.args.unpost),
|
||||
"have_emp": self.args.emp,
|
||||
"have_emp": int(self.args.emp),
|
||||
"md_no_br": int(vf.get("md_no_br") or 0),
|
||||
"ext_th": vf.get("ext_th_d") or {},
|
||||
"sb_md": "" if "no_sb_md" in vf else (vf.get("md_sbf") or "y"),
|
||||
"sba_md": vf.get("md_sba") or "",
|
||||
|
@ -2819,6 +3051,7 @@ class AuthSrv(object):
|
|||
"dvol": self.args.au_vol,
|
||||
"idxh": int(self.args.ih),
|
||||
"dutc": not self.args.localtime,
|
||||
"dfszf": self.args.ui_filesz.strip("-"),
|
||||
"themes": self.args.themes,
|
||||
"turbolvl": self.args.turbo,
|
||||
"nosubtle": self.args.nosubtle,
|
||||
|
@ -2850,6 +3083,18 @@ class AuthSrv(object):
|
|||
zs = str(vol.flags.get("tcolor") or self.args.tcolor)
|
||||
vol.flags["tcolor"] = zs.lstrip("#")
|
||||
|
||||
def setup_auth_ord(self) -> None:
|
||||
ao = [x.strip() for x in self.args.auth_ord.split(",")]
|
||||
if "idp" in ao:
|
||||
zi = ao.index("idp")
|
||||
ao = ao[:zi] + ["idp-hm", "idp-h"] + ao[zi:]
|
||||
zsl = "pw idp-h idp-hm ipu".split()
|
||||
pw, h, hm, ipu = [ao.index(x) if x in ao else 99 for x in zsl]
|
||||
self.args.ao_idp_before_pw = min(h, hm) < pw
|
||||
self.args.ao_h_before_hm = h < hm
|
||||
self.args.ao_ipu_wins = ipu == 0
|
||||
self.args.ao_have_pw = pw < 99 or not self.args.have_idp_hdrs
|
||||
|
||||
def load_idp_db(self, quiet=False) -> None:
|
||||
# mutex me
|
||||
level = self.args.idp_store
|
||||
|
@ -3442,6 +3687,35 @@ class AuthSrv(object):
|
|||
self.log("generated config:\n\n" + "\n".join(ret))
|
||||
|
||||
|
||||
def derive_args(args: argparse.Namespace) -> None:
|
||||
args.have_idp_hdrs = bool(args.idp_h_usr or args.idp_hm_usr)
|
||||
args.have_ipu_or_ipr = bool(args.ipu or args.ipr)
|
||||
|
||||
|
||||
def n_du_who(s: str) -> int:
|
||||
if s == "all":
|
||||
return 9
|
||||
if s == "auth":
|
||||
return 7
|
||||
if s == "w":
|
||||
return 5
|
||||
if s == "rw":
|
||||
return 4
|
||||
if s == "a":
|
||||
return 3
|
||||
return 0
|
||||
|
||||
|
||||
def n_ver_who(s: str) -> int:
|
||||
if s == "all":
|
||||
return 9
|
||||
if s == "auth":
|
||||
return 6
|
||||
if s == "a":
|
||||
return 3
|
||||
return 0
|
||||
|
||||
|
||||
def split_cfg_ln(ln: str) -> dict[str, Any]:
|
||||
# "a, b, c: 3" => {a:true, b:true, c:3}
|
||||
ret = {}
|
||||
|
@ -3474,7 +3748,9 @@ def expand_config_file(
|
|||
|
||||
if os.path.isdir(fp):
|
||||
names = list(sorted(os.listdir(fp)))
|
||||
cnames = [x for x in names if x.lower().endswith(".conf")]
|
||||
cnames = [
|
||||
x for x in names if x.lower().endswith(".conf") and not x.startswith(".")
|
||||
]
|
||||
if not cnames:
|
||||
t = "warning: tried to read config-files from folder '%s' but it does not contain any "
|
||||
if names:
|
||||
|
|
|
@ -2,18 +2,22 @@
|
|||
from __future__ import print_function, unicode_literals
|
||||
|
||||
import os
|
||||
import time
|
||||
|
||||
from ..util import SYMTIME, fsdec, fsenc
|
||||
from . import path as path
|
||||
|
||||
if True: # pylint: disable=using-constant-test
|
||||
from typing import Any, Optional
|
||||
from typing import Any, Optional, Union
|
||||
|
||||
from ..util import NamedLogger
|
||||
|
||||
MKD_755 = {"chmod_d": 0o755}
|
||||
MKD_700 = {"chmod_d": 0o700}
|
||||
UTIME_CLAMPS = ((max, -2147483647), (max, 1), (min, 4294967294), (min, 2147483646))
|
||||
|
||||
_ = (path, MKD_755, MKD_700)
|
||||
__all__ = ["path", "MKD_755", "MKD_700"]
|
||||
_ = (path, MKD_755, MKD_700, UTIME_CLAMPS)
|
||||
__all__ = ["path", "MKD_755", "MKD_700", "UTIME_CLAMPS"]
|
||||
|
||||
# grep -hRiE '(^|[^a-zA-Z_\.-])os\.' . | gsed -r 's/ /\n/g;s/\(/(\n/g' | grep -hRiE '(^|[^a-zA-Z_\.-])os\.' | sort | uniq -c
|
||||
# printf 'os\.(%s)' "$(grep ^def bos/__init__.py | gsed -r 's/^def //;s/\(.*//' | tr '\n' '|' | gsed -r 's/.$//')"
|
||||
|
@ -99,6 +103,40 @@ def utime(
|
|||
return os.utime(fsenc(p), times)
|
||||
|
||||
|
||||
def utime_c(
|
||||
log: Union["NamedLogger", Any], p: str, ts: int, follow_symlinks: bool = True, throw: bool = False
|
||||
) -> Optional[int]:
|
||||
clamp = 0
|
||||
ov = ts
|
||||
bp = fsenc(p)
|
||||
now = int(time.time())
|
||||
while True:
|
||||
try:
|
||||
if SYMTIME:
|
||||
os.utime(bp, (now, ts), follow_symlinks=follow_symlinks)
|
||||
else:
|
||||
os.utime(bp, (now, ts))
|
||||
if clamp:
|
||||
t = "filesystem rejected utime(%r); clamped %s to %s"
|
||||
log(t % (p, ov, ts))
|
||||
return ts
|
||||
except Exception as ex:
|
||||
pv = ts
|
||||
while clamp < len(UTIME_CLAMPS):
|
||||
fun, cv = UTIME_CLAMPS[clamp]
|
||||
ts = fun(ts, cv)
|
||||
clamp += 1
|
||||
if ts != pv:
|
||||
break
|
||||
if clamp >= len(UTIME_CLAMPS):
|
||||
if throw:
|
||||
raise
|
||||
else:
|
||||
t = "could not utime(%r) to %s; %s, %r"
|
||||
log(t % (p, ov, ex, ex))
|
||||
return None
|
||||
|
||||
|
||||
if hasattr(os, "lstat"):
|
||||
|
||||
def lstat(p: str) -> os.stat_result:
|
||||
|
|
|
@ -12,6 +12,7 @@ import queue
|
|||
from .__init__ import ANYWIN
|
||||
from .authsrv import AuthSrv
|
||||
from .broker_util import BrokerCli, ExceptionalQueue, NotExQueue
|
||||
from .fsutil import ramdisk_chk
|
||||
from .httpsrv import HttpSrv
|
||||
from .util import FAKE_MP, Daemon, HMaccas
|
||||
|
||||
|
@ -56,6 +57,7 @@ class MpWorker(BrokerCli):
|
|||
|
||||
# starting to look like a good idea
|
||||
self.asrv = AuthSrv(args, None, False)
|
||||
ramdisk_chk(self.asrv)
|
||||
|
||||
# instantiate all services here (TODO: inheritance?)
|
||||
self.iphash = HMaccas(os.path.join(self.args.E.cfg, "iphash"), 8)
|
||||
|
@ -99,6 +101,7 @@ class MpWorker(BrokerCli):
|
|||
if dest == "reload":
|
||||
self.logw("mpw.asrv reloading")
|
||||
self.asrv.reload()
|
||||
ramdisk_chk(self.asrv)
|
||||
self.logw("mpw.asrv reloaded")
|
||||
continue
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
from __future__ import print_function, unicode_literals
|
||||
|
||||
import argparse
|
||||
import traceback
|
||||
|
||||
from queue import Queue
|
||||
|
||||
|
|
|
@ -130,6 +130,7 @@ def _gen_srv(log: "RootLogger", args, netdevs: dict[str, Netdev]):
|
|||
nlog: "NamedLogger" = lambda msg, c=0: log("cert-gen-srv", msg, c)
|
||||
|
||||
names = args.crt_ns.split(",") if args.crt_ns else []
|
||||
names = [x.strip() for x in names]
|
||||
if not args.crt_exact:
|
||||
for n in names[:]:
|
||||
names.append("*.{}".format(n))
|
||||
|
|
|
@ -44,6 +44,7 @@ def vf_bmap() -> dict[str, str]:
|
|||
"gsel",
|
||||
"hardlink",
|
||||
"magic",
|
||||
"md_no_br",
|
||||
"no_db_ip",
|
||||
"no_sb_md",
|
||||
"no_sb_lg",
|
||||
|
@ -51,11 +52,14 @@ def vf_bmap() -> dict[str, str]:
|
|||
"og",
|
||||
"og_no_head",
|
||||
"og_s_title",
|
||||
"opds",
|
||||
"rand",
|
||||
"reflink",
|
||||
"rm_partial",
|
||||
"rmagic",
|
||||
"rss",
|
||||
"wo_up_readme",
|
||||
"wram",
|
||||
"xdev",
|
||||
"xlink",
|
||||
"xvol",
|
||||
|
@ -80,12 +84,16 @@ def vf_vmap() -> dict[str, str]:
|
|||
}
|
||||
for k in (
|
||||
"bup_ck",
|
||||
"casechk",
|
||||
"chmod_d",
|
||||
"chmod_f",
|
||||
"dbd",
|
||||
"du_who",
|
||||
"ufavico",
|
||||
"forget_ip",
|
||||
"hsortn",
|
||||
"html_head",
|
||||
"html_head_s",
|
||||
"lg_sbf",
|
||||
"md_sbf",
|
||||
"lg_sba",
|
||||
|
@ -102,10 +110,12 @@ def vf_vmap() -> dict[str, str]:
|
|||
"og_title_i",
|
||||
"og_tpl",
|
||||
"og_ua",
|
||||
"opds_exts",
|
||||
"put_ck",
|
||||
"put_name",
|
||||
"mv_retry",
|
||||
"rm_retry",
|
||||
"shr_who",
|
||||
"sort",
|
||||
"tail_fd",
|
||||
"tail_rate",
|
||||
|
@ -183,8 +193,10 @@ flagcats = {
|
|||
"chmod_f=644": "unix-permission for new files",
|
||||
"uid=573": "change owner of new files/folders to unix-user 573",
|
||||
"gid=999": "change owner of new files/folders to unix-group 999",
|
||||
"wram": "allow uploading into ramdisks",
|
||||
"sparse": "force use of sparse files, mainly for s3-backed storage",
|
||||
"nosparse": "deny use of sparse files, mainly for slow storage",
|
||||
"rm_partial": "delete unfinished uploads from HDD when they timeout",
|
||||
"daw": "enable full WebDAV write support (dangerous);\nPUT-operations will now \033[1;31mOVERWRITE\033[0;35m existing files",
|
||||
"nosub": "forces all uploads into the top folder of the vfs",
|
||||
"magic": "enables filetype detection for nameless uploads",
|
||||
|
@ -213,6 +225,7 @@ flagcats = {
|
|||
"upload rotation\n(moves all uploads into the specified folder structure)": {
|
||||
"rotn=100,3": "3 levels of subfolders with 100 entries in each",
|
||||
"rotf=%Y-%m/%d-%H": "date-formatted organizing",
|
||||
"rotf_tz=Europe/Oslo": "timezone (default=UTC)",
|
||||
"lifetime=3600": "uploads are deleted after 1 hour",
|
||||
},
|
||||
"database, general": {
|
||||
|
@ -241,6 +254,7 @@ flagcats = {
|
|||
"no_db_ip": "never store uploader-IP in the db; disables unpost",
|
||||
"fat32": "avoid excessive reindexing on android sdcardfs",
|
||||
"dbd=[acid|swal|wal|yolo]": "database speed-durability tradeoff",
|
||||
"casechk=auto": "actively prevent case-insensitive filesystem? y/n",
|
||||
"xlink": "cross-volume dupe detection / linking (dangerous)",
|
||||
"xdev": "do not descend into other filesystems",
|
||||
"xvol": "do not follow symlinks leaving the volume root",
|
||||
|
@ -291,10 +305,13 @@ flagcats = {
|
|||
"sort": "default sort order",
|
||||
"nsort": "natural-sort of leading digits in filenames",
|
||||
"hsortn": "number of sort-rules to add to media URLs",
|
||||
"ufavico=URL": "per-volume favicon (.ico/png/gif/svg)",
|
||||
"unlist": "dont list files matching REGEX",
|
||||
"html_head=TXT": "includes TXT in the <head>, or @PATH for file at PATH",
|
||||
"html_head_s=TXT": "additional static text in the html <head>",
|
||||
"tcolor=#fc0": "theme color (a hint for webbrowsers, discord, etc.)",
|
||||
"nodirsz": "don't show total folder size",
|
||||
"du_who=all": "show disk-usage info to everyone",
|
||||
"robots": "allows indexing by search engines (default)",
|
||||
"norobots": "kindly asks search engines to leave",
|
||||
"unlistcr": "don't list read-access in controlpanel",
|
||||
|
@ -323,7 +340,12 @@ flagcats = {
|
|||
"og_no_head": "you want to add tags manually with og_tpl",
|
||||
"og_ua": "if defined: only send OG html if useragent matches this regex",
|
||||
},
|
||||
"opds": {
|
||||
"opds": "enable OPDS",
|
||||
"opds_exts": "file formats to list in OPDS feeds; leave empty to show everything",
|
||||
},
|
||||
"textfiles": {
|
||||
"md_no_br": "newline only on double-newline or two tailing spaces",
|
||||
"md_hist": "where to put markdown backups; s=subfolder, v=volHist, n=nope",
|
||||
"exp": "enable textfile expansion; see --help-exp",
|
||||
"exp_md": "placeholders to expand in markdown files; see --help",
|
||||
|
@ -346,6 +368,7 @@ flagcats = {
|
|||
"dky": 'allow seeing files (not folders) inside a specific folder\nwith "g" perm, and does not require a valid dirkey to do so',
|
||||
"rss": "allow '?rss' URL suffix (experimental)",
|
||||
"rmagic": "expensive analysis for mimetype accuracy",
|
||||
"shr_who=auth": "who can create shares? no/auth/a",
|
||||
"unp_who=2": "unpost only if same... 1=ip+name, 2=ip, 3=name",
|
||||
"ups_who=2": "restrict viewing the list of recent uploads",
|
||||
"zip_who=2": "restrict access to download-as-zip/tar",
|
||||
|
|
|
@ -7,7 +7,7 @@ import re
|
|||
import time
|
||||
|
||||
from .__init__ import ANYWIN, MACOS
|
||||
from .authsrv import AXS, VFS
|
||||
from .authsrv import AXS, VFS, AuthSrv
|
||||
from .bos import bos
|
||||
from .util import chkcmd, min_ex, undot
|
||||
|
||||
|
@ -18,22 +18,25 @@ if True: # pylint: disable=using-constant-test
|
|||
|
||||
|
||||
class Fstab(object):
|
||||
def __init__(self, log: "RootLogger", args: argparse.Namespace):
|
||||
def __init__(self, log: "RootLogger", args: argparse.Namespace, verbose: bool):
|
||||
self.log_func = log
|
||||
self.verbose = verbose
|
||||
|
||||
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.cache: dict[str, tuple[str, str]] = {}
|
||||
self.age = 0.0
|
||||
self.maxage = args.mtab_age
|
||||
|
||||
def log(self, msg: str, c: Union[int, str] = 0) -> None:
|
||||
if not c or self.verbose:
|
||||
return
|
||||
self.log_func("fstab", msg, c)
|
||||
|
||||
def get(self, path: str) -> str:
|
||||
def get(self, path: str) -> tuple[str, str]:
|
||||
now = time.time()
|
||||
if now - self.age > self.maxage or len(self.cache) > 9000:
|
||||
self.age = now
|
||||
|
@ -41,6 +44,7 @@ class Fstab(object):
|
|||
self.tab = None
|
||||
self.cache = {}
|
||||
|
||||
mp = ""
|
||||
fs = "ext4"
|
||||
msg = "failed to determine filesystem at %r; assuming %s\n%s"
|
||||
|
||||
|
@ -50,7 +54,7 @@ class Fstab(object):
|
|||
path = self._winpath(path)
|
||||
except:
|
||||
self.log(msg % (path, fs, min_ex()), 3)
|
||||
return fs
|
||||
return fs, ""
|
||||
|
||||
path = undot(path)
|
||||
try:
|
||||
|
@ -59,14 +63,14 @@ class Fstab(object):
|
|||
pass
|
||||
|
||||
try:
|
||||
fs = self.get_w32(path) if ANYWIN else self.get_unix(path)
|
||||
fs, mp = self.get_w32(path) if ANYWIN else self.get_unix(path)
|
||||
except:
|
||||
self.log(msg % (path, fs, min_ex()), 3)
|
||||
|
||||
fs = fs.lower()
|
||||
self.cache[path] = fs
|
||||
self.log("found %s at %r" % (fs, path))
|
||||
return fs
|
||||
self.cache[path] = (fs, mp)
|
||||
self.log("found %s at %r, %r" % (fs, mp, path))
|
||||
return fs, mp
|
||||
|
||||
def _winpath(self, path: str) -> str:
|
||||
# try to combine volume-label + st_dev (vsn)
|
||||
|
@ -81,34 +85,49 @@ class Fstab(object):
|
|||
self.tab = VFS(self.log_func, "idk", "/", "/", AXS(), {})
|
||||
self.trusted = False
|
||||
|
||||
def build_tab(self) -> None:
|
||||
self.log("inspecting mtab for changes")
|
||||
|
||||
def _from_sp_mount(self) -> dict[str, str]:
|
||||
sptn = r"^.*? on (.*) type ([^ ]+) \(.*"
|
||||
if MACOS:
|
||||
sptn = r"^.*? on (.*) \(([^ ]+), .*"
|
||||
|
||||
ptn = re.compile(sptn)
|
||||
so, _ = chkcmd(["mount"])
|
||||
tab1: list[tuple[str, str]] = []
|
||||
atab = []
|
||||
dtab: dict[str, str] = {}
|
||||
for ln in so.split("\n"):
|
||||
m = ptn.match(ln)
|
||||
if not m:
|
||||
continue
|
||||
|
||||
zs1, zs2 = m.groups()
|
||||
tab1.append((str(zs1), str(zs2)))
|
||||
atab.append(ln)
|
||||
dtab[str(zs1)] = str(zs2)
|
||||
|
||||
return dtab
|
||||
|
||||
def _from_proc(self) -> dict[str, str]:
|
||||
ret: dict[str, str] = {}
|
||||
with open("/proc/self/mounts", "rb", 262144) as f:
|
||||
src = f.read(262144).decode("utf-8", "replace").split("\n")
|
||||
for zsl in [x.split(" ") for x in src]:
|
||||
if len(zsl) < 3:
|
||||
continue
|
||||
zs = zsl[1]
|
||||
zs = zs.replace("\\011", "\t").replace("\\040", " ").replace("\\134", "\\")
|
||||
ret[zs] = zsl[2]
|
||||
return ret
|
||||
|
||||
def build_tab(self) -> None:
|
||||
self.log("inspecting mtab for changes")
|
||||
dtab = self._from_sp_mount() if MACOS else self._from_proc()
|
||||
|
||||
# keep empirically-correct values if mounttab unchanged
|
||||
srctab = "\n".join(sorted(atab))
|
||||
srctab = str(sorted(dtab.items()))
|
||||
if srctab == self.srctab:
|
||||
self.tab = self.oldtab
|
||||
return
|
||||
|
||||
self.log("mtab has changed; reevaluating support for sparse files")
|
||||
|
||||
tab1 = list(dtab.items())
|
||||
tab1.sort(key=lambda x: (len(x[0]), x[0]))
|
||||
path1, fs1 = tab1[0]
|
||||
tab = VFS(self.log_func, fs1, path1, path1, AXS(), {})
|
||||
|
@ -146,7 +165,7 @@ class Fstab(object):
|
|||
vn.realpath = ptn.sub(nval, vn.realpath)
|
||||
visit.extend(list(vn.nodes.values()))
|
||||
|
||||
def get_unix(self, path: str) -> str:
|
||||
def get_unix(self, path: str) -> tuple[str, str]:
|
||||
if not self.tab:
|
||||
try:
|
||||
self.build_tab()
|
||||
|
@ -155,20 +174,44 @@ class Fstab(object):
|
|||
# prisonparty or other restrictive environment
|
||||
if not self.warned:
|
||||
self.warned = True
|
||||
self.log("failed to build tab:\n{}".format(min_ex()), 3)
|
||||
t = "failed to associate fs-mounts with the VFS (this is fine):\n%s"
|
||||
self.log(t % (min_ex(),), 6)
|
||||
self.build_fallback()
|
||||
|
||||
assert self.tab # !rm
|
||||
ret = self.tab._find(path)[0]
|
||||
if self.trusted or path == ret.vpath:
|
||||
return ret.realpath.split("/")[0]
|
||||
return ret.realpath.split("/")[0], ret.vpath
|
||||
else:
|
||||
return "idk"
|
||||
return "idk", ""
|
||||
|
||||
def get_w32(self, path: str) -> str:
|
||||
def get_w32(self, path: str) -> tuple[str, str]:
|
||||
if not self.tab:
|
||||
self.build_fallback()
|
||||
|
||||
assert self.tab # !rm
|
||||
ret = self.tab._find(path)[0]
|
||||
return ret.realpath
|
||||
return ret.realpath, ""
|
||||
|
||||
|
||||
def ramdisk_chk(asrv: AuthSrv) -> None:
|
||||
# should have been in authsrv but that's a circular import
|
||||
mods = []
|
||||
ramfs = ("tmpfs", "overlay")
|
||||
log = asrv.log_func or print
|
||||
fstab = Fstab(log, asrv.args, False)
|
||||
for vn in asrv.vfs.all_nodes.values():
|
||||
if not vn.axs.uwrite or "wram" in vn.flags:
|
||||
continue
|
||||
ap = vn.realpath
|
||||
if not ap or os.path.isfile(ap):
|
||||
continue
|
||||
fs, mp = fstab.get(ap)
|
||||
mp = "/" + mp.strip("/")
|
||||
if fs == "tmpfs" or (mp == "/" and fs in ramfs):
|
||||
mods.append((vn.vpath, ap, fs, mp))
|
||||
vn.axs.uwrite.clear()
|
||||
if mods:
|
||||
t = "WARNING: write-access was removed from the following volumes because they are not mapped to an actual HDD for storage! All uploaded data would live in RAM only, and all uploaded files would be LOST on next reboot. To allow uploading and ignore this hazard, enable the 'wram' option (global/volflag). List of affected volumes:"
|
||||
t2 = ["\n volume=[/%s], abspath=%r, type=%s, root=%r" % x for x in mods]
|
||||
log("vfs", t + "".join(t2) + "\n", 1)
|
||||
|
|
|
@ -68,13 +68,13 @@ class FtpAuth(DummyAuthorizer):
|
|||
if ip.startswith("::ffff:"):
|
||||
ip = ip[7:]
|
||||
|
||||
ip = ipnorm(ip)
|
||||
ipn = ipnorm(ip)
|
||||
bans = self.hub.bans
|
||||
if ip in bans:
|
||||
rt = bans[ip] - time.time()
|
||||
if ipn in bans:
|
||||
rt = bans[ipn] - time.time()
|
||||
if rt < 0:
|
||||
logging.info("client unbanned")
|
||||
del bans[ip]
|
||||
del bans[ipn]
|
||||
else:
|
||||
raise AuthenticationFailed("banned")
|
||||
|
||||
|
@ -152,10 +152,6 @@ class FtpFs(AbstractedFS):
|
|||
self.cwd = "/" # pyftpdlib convention of leading slash
|
||||
self.root = "/var/lib/empty"
|
||||
|
||||
self.can_read = self.can_write = self.can_move = False
|
||||
self.can_delete = self.can_get = self.can_upget = False
|
||||
self.can_admin = self.can_dot = False
|
||||
|
||||
self.listdirinfo = self.listdir
|
||||
self.chdir(".")
|
||||
|
||||
|
@ -206,6 +202,9 @@ class FtpFs(AbstractedFS):
|
|||
if r and not cr or w and not cw or m and not cm or d and not cd:
|
||||
raise FSE(t.format(vpath), 1)
|
||||
|
||||
if "bcasechk" in vfs.flags and not vfs.casechk(rem, True):
|
||||
raise FSE("No such file or directory", 1)
|
||||
|
||||
return os.path.join(vfs.realpath, rem), vfs, rem
|
||||
except Pebkac as ex:
|
||||
raise FSE(str(ex))
|
||||
|
@ -218,7 +217,7 @@ class FtpFs(AbstractedFS):
|
|||
m: bool = False,
|
||||
d: bool = False,
|
||||
) -> tuple[str, VFS, str]:
|
||||
return self.v2a(os.path.join(self.cwd, vpath), r, w, m, d)
|
||||
return self.v2a(join(self.cwd, vpath), r, w, m, d)
|
||||
|
||||
def ftp2fs(self, ftppath: str) -> str:
|
||||
# return self.v2a(ftppath)
|
||||
|
@ -280,6 +279,10 @@ class FtpFs(AbstractedFS):
|
|||
def chdir(self, path: str) -> None:
|
||||
nwd = join(self.cwd, path)
|
||||
vfs, rem = self.hub.asrv.vfs.get(nwd, self.uname, False, False)
|
||||
if not vfs.realpath:
|
||||
self.cwd = nwd
|
||||
return
|
||||
|
||||
ap = vfs.canonical(rem)
|
||||
try:
|
||||
st = bos.stat(ap)
|
||||
|
@ -289,24 +292,11 @@ class FtpFs(AbstractedFS):
|
|||
# returning 550 is library-default and suitable
|
||||
raise FSE("No such file or directory")
|
||||
|
||||
if vfs.realpath:
|
||||
avfs = vfs.chk_ap(ap, st)
|
||||
if not avfs:
|
||||
raise FSE("Permission denied", 1)
|
||||
else:
|
||||
avfs = vfs
|
||||
avfs = vfs.chk_ap(ap, st)
|
||||
if not avfs:
|
||||
raise FSE("Permission denied", 1)
|
||||
|
||||
self.cwd = nwd
|
||||
(
|
||||
self.can_read,
|
||||
self.can_write,
|
||||
self.can_move,
|
||||
self.can_delete,
|
||||
self.can_get,
|
||||
self.can_upget,
|
||||
self.can_admin,
|
||||
self.can_dot,
|
||||
) = avfs.can_access("", self.h.uname)
|
||||
|
||||
def mkdir(self, path: str) -> None:
|
||||
ap, vfs, _ = self.rv2a(path, w=True)
|
||||
|
@ -329,7 +319,7 @@ class FtpFs(AbstractedFS):
|
|||
vfs_ls = [x[0] for x in vfs_ls1]
|
||||
vfs_ls.extend(vfs_virt.keys())
|
||||
|
||||
if not self.can_dot:
|
||||
if self.uname not in vfs.axs.udot:
|
||||
vfs_ls = exclude_dotfiles(vfs_ls)
|
||||
|
||||
vfs_ls.sort()
|
||||
|
@ -377,16 +367,13 @@ class FtpFs(AbstractedFS):
|
|||
raise FSE(str(ex))
|
||||
|
||||
def rename(self, src: str, dst: str) -> None:
|
||||
if not self.can_move:
|
||||
raise FSE("Not allowed for user " + self.h.uname)
|
||||
|
||||
if self.args.no_mv:
|
||||
raise FSE("The rename/move feature is disabled in server config")
|
||||
|
||||
svp = join(self.cwd, src).lstrip("/")
|
||||
dvp = join(self.cwd, dst).lstrip("/")
|
||||
try:
|
||||
self.hub.up2k.handle_mv(self.uname, self.h.cli_ip, svp, dvp)
|
||||
self.hub.up2k.handle_mv("", self.uname, self.h.cli_ip, svp, dvp)
|
||||
except Exception as ex:
|
||||
raise FSE(str(ex))
|
||||
|
||||
|
@ -409,12 +396,8 @@ class FtpFs(AbstractedFS):
|
|||
return st
|
||||
|
||||
def utime(self, path: str, timeval: float) -> None:
|
||||
try:
|
||||
ap = self.rv2a(path, w=True)[0]
|
||||
return bos.utime(ap, (int(time.time()), int(timeval)))
|
||||
except Exception as ex:
|
||||
logging.error("ftp.utime: %s, %r", ex, ex)
|
||||
raise
|
||||
ap = self.rv2a(path, w=True)[0]
|
||||
bos.utime_c(logging.warning, ap, int(timeval), False)
|
||||
|
||||
def lstat(self, path: str) -> os.stat_result:
|
||||
ap = self.rv2a(path)[0]
|
||||
|
@ -510,24 +493,30 @@ class FtpHandler(FTPHandler):
|
|||
return
|
||||
self.vfs_map[ap] = vp
|
||||
xbu = vfs.flags.get("xbu")
|
||||
if xbu and not runhook(
|
||||
None,
|
||||
None,
|
||||
self.hub.up2k,
|
||||
"xbu.ftpd",
|
||||
xbu,
|
||||
ap,
|
||||
vp,
|
||||
"",
|
||||
self.uname,
|
||||
self.hub.asrv.vfs.get_perms(vp, self.uname),
|
||||
0,
|
||||
0,
|
||||
self.cli_ip,
|
||||
time.time(),
|
||||
"",
|
||||
):
|
||||
raise FSE("Upload blocked by xbu server config")
|
||||
if xbu:
|
||||
hr = runhook(
|
||||
None,
|
||||
None,
|
||||
self.hub.up2k,
|
||||
"xbu.ftpd",
|
||||
xbu,
|
||||
ap,
|
||||
vp,
|
||||
"",
|
||||
self.uname,
|
||||
self.hub.asrv.vfs.get_perms(vp, self.uname),
|
||||
0,
|
||||
0,
|
||||
self.cli_ip,
|
||||
time.time(),
|
||||
"",
|
||||
)
|
||||
t = hr.get("rejectmsg") or ""
|
||||
if t or not hr:
|
||||
if not t:
|
||||
t = "Upload blocked by xbu server config: %r" % (vp,)
|
||||
self.respond("550 %s" % (t,), logging.info)
|
||||
return
|
||||
|
||||
# print("ftp_STOR: {} {} => {}".format(vp, mode, ap))
|
||||
ret = FTPHandler.ftp_STOR(self, file, mode)
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -187,6 +187,7 @@ class HttpSrv(object):
|
|||
"svcs",
|
||||
]
|
||||
self.j2 = {x: env.get_template(x + ".html") for x in jn}
|
||||
self.j2["opds"] = env.get_template("opds.xml")
|
||||
self.prism = has_resource(self.E, "web/deps/prism.js.gz")
|
||||
|
||||
if self.args.ipu:
|
||||
|
@ -571,7 +572,7 @@ class HttpSrv(object):
|
|||
|
||||
v = self.E.t0
|
||||
try:
|
||||
with os.scandir(os.path.join(self.E.mod, "web")) as dh:
|
||||
with os.scandir(self.E.mod_ + "web") as dh:
|
||||
for fh in dh:
|
||||
inf = fh.stat()
|
||||
v = max(v, inf.st_mtime)
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
from __future__ import print_function, unicode_literals
|
||||
|
||||
import errno
|
||||
import os
|
||||
import random
|
||||
import select
|
||||
import socket
|
||||
|
@ -12,22 +13,52 @@ from ipaddress import IPv4Network, IPv6Network
|
|||
from .__init__ import TYPE_CHECKING
|
||||
from .__init__ import unicode as U
|
||||
from .multicast import MC_Sck, MCast
|
||||
from .stolen.dnslib import AAAA
|
||||
from .stolen.dnslib import CLASS as DC
|
||||
from .stolen.dnslib import (
|
||||
NSEC,
|
||||
PTR,
|
||||
QTYPE,
|
||||
RR,
|
||||
SRV,
|
||||
TXT,
|
||||
A,
|
||||
DNSHeader,
|
||||
DNSQuestion,
|
||||
DNSRecord,
|
||||
set_avahi_379,
|
||||
)
|
||||
from .util import CachedSet, Daemon, Netdev, list_ips, min_ex
|
||||
from .util import IP6_LL, CachedSet, Daemon, Netdev, list_ips, min_ex
|
||||
|
||||
try:
|
||||
if os.getenv("PRTY_SYS_ALL") or os.getenv("PRTY_SYS_DNSLIB"):
|
||||
raise ImportError()
|
||||
from .stolen.dnslib import (
|
||||
AAAA,
|
||||
)
|
||||
from .stolen.dnslib import CLASS as DC
|
||||
from .stolen.dnslib import (
|
||||
NSEC,
|
||||
PTR,
|
||||
QTYPE,
|
||||
RR,
|
||||
SRV,
|
||||
TXT,
|
||||
A,
|
||||
DNSHeader,
|
||||
DNSQuestion,
|
||||
DNSRecord,
|
||||
set_avahi_379,
|
||||
)
|
||||
|
||||
DNS_VND = True
|
||||
except ImportError:
|
||||
DNS_VND = False
|
||||
from dnslib import (
|
||||
AAAA,
|
||||
)
|
||||
from dnslib import CLASS as DC
|
||||
from dnslib import (
|
||||
NSEC,
|
||||
PTR,
|
||||
QTYPE,
|
||||
RR,
|
||||
SRV,
|
||||
TXT,
|
||||
A,
|
||||
Bimap,
|
||||
DNSHeader,
|
||||
DNSQuestion,
|
||||
DNSRecord,
|
||||
)
|
||||
|
||||
DC.forward[0x8001] = "F_IN"
|
||||
DC.reverse["F_IN"] = 0x8001
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .svchub import SvcHub
|
||||
|
@ -35,6 +66,11 @@ if TYPE_CHECKING:
|
|||
if True: # pylint: disable=using-constant-test
|
||||
from typing import Any, Optional, Union
|
||||
|
||||
if os.getenv("PRTY_MODSPEC"):
|
||||
from inspect import getsourcefile
|
||||
|
||||
print("PRTY_MODSPEC: dnslib:", getsourcefile(A))
|
||||
|
||||
|
||||
MDNS4 = "224.0.0.251"
|
||||
MDNS6 = "ff02::fb"
|
||||
|
@ -73,7 +109,7 @@ class MDNS(MCast):
|
|||
self.ngen = ngen
|
||||
self.ttl = 300
|
||||
|
||||
if not self.args.zm_nwa_1:
|
||||
if not self.args.zm_nwa_1 and DNS_VND:
|
||||
set_avahi_379()
|
||||
|
||||
zs = self.args.zm_fqdn or (self.args.name + ".local")
|
||||
|
@ -100,9 +136,14 @@ class MDNS(MCast):
|
|||
self.log_func(self.logsrc, msg, c)
|
||||
|
||||
def build_svcs(self) -> tuple[dict[str, dict[str, Any]], set[str]]:
|
||||
ar = self.args
|
||||
zms = self.args.zms
|
||||
http = {"port": 80 if 80 in self.args.p else self.args.p[0]}
|
||||
https = {"port": 443 if 443 in self.args.p else self.args.p[0]}
|
||||
|
||||
zi = ar.zm_http
|
||||
http = {"port": zi if zi != -1 else 80 if 80 in ar.p else ar.p[0]}
|
||||
zi = ar.zm_https
|
||||
https = {"port": zi if zi != -1 else 443 if 443 in ar.p else ar.p[0]}
|
||||
|
||||
webdav = http.copy()
|
||||
webdavs = https.copy()
|
||||
webdav["u"] = webdavs["u"] = "u" # KDE requires username
|
||||
|
@ -127,16 +168,16 @@ class MDNS(MCast):
|
|||
|
||||
svcs: dict[str, dict[str, Any]] = {}
|
||||
|
||||
if "d" in zms:
|
||||
if "d" in zms and http["port"]:
|
||||
svcs["_webdav._tcp.local."] = webdav
|
||||
|
||||
if "D" in zms:
|
||||
if "D" in zms and https["port"]:
|
||||
svcs["_webdavs._tcp.local."] = webdavs
|
||||
|
||||
if "h" in zms:
|
||||
if "h" in zms and http["port"]:
|
||||
svcs["_http._tcp.local."] = http
|
||||
|
||||
if "H" in zms:
|
||||
if "H" in zms and https["port"]:
|
||||
svcs["_https._tcp.local."] = https
|
||||
|
||||
if "f" in zms.lower():
|
||||
|
@ -375,7 +416,7 @@ class MDNS(MCast):
|
|||
cip = addr[0]
|
||||
v6 = ":" in cip
|
||||
if (cip.startswith("169.254") and not self.ll_ok) or (
|
||||
v6 and not cip.startswith("fe80")
|
||||
v6 and not cip.startswith(IP6_LL)
|
||||
):
|
||||
return
|
||||
|
||||
|
|
|
@ -178,6 +178,7 @@ def au_unpk(
|
|||
|
||||
elif pk == "epub":
|
||||
fi = get_cover_from_epub(log, abspath)
|
||||
assert fi # !rm
|
||||
|
||||
else:
|
||||
raise Exception("unknown compression %s" % (pk,))
|
||||
|
@ -199,9 +200,10 @@ def au_unpk(
|
|||
|
||||
except Exception as ex:
|
||||
if ret:
|
||||
t = "failed to decompress audio file %r: %r"
|
||||
t = "failed to decompress file %r: %r"
|
||||
log(t % (abspath, ex))
|
||||
wunlink(log, ret, vn.flags if vn else VF_CAREFUL)
|
||||
return ""
|
||||
|
||||
return abspath
|
||||
|
||||
|
@ -421,10 +423,17 @@ def get_cover_from_epub(log: "NamedLogger", abspath: str) -> Optional[IO[bytes]]
|
|||
# This might be an EPUB2 file, try the legacy way of specifying covers
|
||||
coverimage_path = _get_cover_from_epub2(log, package_root, package_ns)
|
||||
|
||||
if not coverimage_path:
|
||||
raise Exception("no cover inside epub")
|
||||
|
||||
# This url is either absolute (in the .epub) or relative to the package document
|
||||
adjusted_cover_path = urljoin(rootfile_path, coverimage_path)
|
||||
|
||||
return z.open(adjusted_cover_path)
|
||||
try:
|
||||
return z.open(adjusted_cover_path)
|
||||
except KeyError:
|
||||
t = "epub: cover specified in package document, but doesn't exist: %s"
|
||||
log(t % (adjusted_cover_path,))
|
||||
|
||||
|
||||
def _get_cover_from_epub2(
|
||||
|
@ -432,9 +441,8 @@ def _get_cover_from_epub2(
|
|||
) -> Optional[str]:
|
||||
# <meta name="cover" content="id-to-cover-image"> in <metadata>, then
|
||||
# <item> in <manifest>
|
||||
cover_id = package_root.find("./metadata/meta[@name='cover']", package_ns).get(
|
||||
"content"
|
||||
)
|
||||
xn = package_root.find("./metadata/meta[@name='cover']", package_ns)
|
||||
cover_id = xn.get("content") if xn is not None else None
|
||||
|
||||
if not cover_id:
|
||||
return None
|
||||
|
@ -510,7 +518,6 @@ class MTag(object):
|
|||
"album-artist",
|
||||
"tpe2",
|
||||
"aart",
|
||||
"conductor",
|
||||
"organization",
|
||||
"band",
|
||||
],
|
||||
|
@ -644,6 +651,9 @@ class MTag(object):
|
|||
return self._get(abspath)
|
||||
|
||||
ap = au_unpk(self.log, self.args.au_unpk, abspath)
|
||||
if not ap:
|
||||
return {}
|
||||
|
||||
ret = self._get(ap)
|
||||
if ap != abspath:
|
||||
wunlink(self.log, ap, VF_CAREFUL)
|
||||
|
@ -749,6 +759,9 @@ class MTag(object):
|
|||
ap = abspath
|
||||
|
||||
ret: dict[str, Any] = {}
|
||||
if not ap:
|
||||
return ret
|
||||
|
||||
for tagname, parser in sorted(parsers.items(), key=lambda x: (x[1].pri, x[0])):
|
||||
try:
|
||||
cmd = [parser.bin, ap]
|
||||
|
|
|
@ -15,7 +15,7 @@ from ipaddress import (
|
|||
)
|
||||
|
||||
from .__init__ import MACOS, TYPE_CHECKING
|
||||
from .util import Daemon, Netdev, find_prefix, min_ex, spack
|
||||
from .util import IP6_LL, IP64_LL, Daemon, Netdev, find_prefix, min_ex, spack
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .svchub import SvcHub
|
||||
|
@ -96,7 +96,10 @@ class MCast(object):
|
|||
def create_servers(self) -> list[str]:
|
||||
bound: list[str] = []
|
||||
netdevs = self.hub.tcpsrv.netdevs
|
||||
ips = [x[0] for x in self.hub.tcpsrv.bound]
|
||||
blist = self.hub.tcpsrv.bound
|
||||
if self.args.http_no_tcp:
|
||||
blist = self.hub.tcpsrv.seen_eps
|
||||
ips = [x[0] for x in blist]
|
||||
|
||||
if "::" in ips:
|
||||
ips = [x for x in ips if x != "::"] + list(
|
||||
|
@ -145,7 +148,7 @@ class MCast(object):
|
|||
all_selected = ips[:]
|
||||
|
||||
# discard non-linklocal ipv6
|
||||
ips = [x for x in ips if ":" not in x or x.startswith("fe80")]
|
||||
ips = [x for x in ips if ":" not in x or x.startswith(IP6_LL)]
|
||||
|
||||
if not ips:
|
||||
raise NoIPs()
|
||||
|
@ -183,7 +186,7 @@ class MCast(object):
|
|||
srv.ips[oth_ip.split("/")[0]] = ipaddress.ip_network(oth_ip, False)
|
||||
|
||||
# gvfs breaks if a linklocal ip appears in a dns reply
|
||||
ll = {k: v for k, v in srv.ips.items() if k.startswith(("169.254", "fe80"))}
|
||||
ll = {k: v for k, v in srv.ips.items() if k.startswith(IP64_LL)}
|
||||
rt = {k: v for k, v in srv.ips.items() if k not in ll}
|
||||
|
||||
if self.args.ll or not rt:
|
||||
|
|
|
@ -25,6 +25,7 @@ class PWHash(object):
|
|||
self.args = args
|
||||
|
||||
zsl = args.ah_alg.split(",")
|
||||
zsl = [x.strip() for x in zsl]
|
||||
alg = zsl[0]
|
||||
if alg == "none":
|
||||
alg = ""
|
||||
|
|
112
copyparty/qrkode.py
Normal file
112
copyparty/qrkode.py
Normal file
|
@ -0,0 +1,112 @@
|
|||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
import os
|
||||
|
||||
try:
|
||||
if os.getenv("PRTY_SYS_ALL") or os.getenv("PRTY_SYS_QRCG"):
|
||||
raise ImportError()
|
||||
from .stolen.qrcodegen import QrCode
|
||||
|
||||
qrgen = QrCode.encode_binary
|
||||
VENDORED = True
|
||||
except ImportError:
|
||||
VENDORED = False
|
||||
from qrcodegen import QrCode
|
||||
|
||||
if os.getenv("PRTY_MODSPEC"):
|
||||
from inspect import getsourcefile
|
||||
|
||||
print("PRTY_MODSPEC: qrcode:", getsourcefile(QrCode))
|
||||
|
||||
if True: # pylint: disable=using-constant-test
|
||||
import typing
|
||||
from typing import Any, Optional, Sequence, Union
|
||||
|
||||
|
||||
if not VENDORED:
|
||||
|
||||
def _qrgen(data: Union[bytes, Sequence[int]]) -> "QrCode":
|
||||
ret = None
|
||||
V = QrCode.Ecc
|
||||
for e in [V.HIGH, V.QUARTILE, V.MEDIUM, V.LOW]:
|
||||
qr = QrCode.encode_binary(data, e)
|
||||
qr.size = qr._size
|
||||
qr.modules = qr._modules
|
||||
if not ret or ret.size > qr.size:
|
||||
ret = qr
|
||||
return ret
|
||||
|
||||
qrgen = _qrgen
|
||||
|
||||
|
||||
def qr2txt(qr: QrCode, zoom: int = 1, pad: int = 4) -> str:
|
||||
tab = qr.modules
|
||||
sz = qr.size
|
||||
if sz % 2 and zoom == 1:
|
||||
tab.append([False] * sz)
|
||||
|
||||
tab = [[False] * sz] * pad + tab + [[False] * sz] * pad
|
||||
tab = [[False] * pad + x + [False] * pad for x in tab]
|
||||
|
||||
rows: list[str] = []
|
||||
if zoom == 1:
|
||||
for y in range(0, len(tab), 2):
|
||||
row = ""
|
||||
for x in range(len(tab[y])):
|
||||
v = 2 if tab[y][x] else 0
|
||||
v += 1 if tab[y + 1][x] else 0
|
||||
row += " ▄▀█"[v]
|
||||
rows.append(row)
|
||||
else:
|
||||
for tr in tab:
|
||||
row = ""
|
||||
for zb in tr:
|
||||
row += " █"[int(zb)] * 2
|
||||
rows.append(row)
|
||||
|
||||
return "\n".join(rows)
|
||||
|
||||
|
||||
def qr2png(
|
||||
qr: QrCode,
|
||||
zoom: int,
|
||||
pad: int,
|
||||
bg: Optional[tuple[int, int, int]],
|
||||
fg: Optional[tuple[int, int, int]],
|
||||
ap: str,
|
||||
) -> None:
|
||||
from PIL import Image
|
||||
|
||||
tab = qr.modules
|
||||
sz = qr.size
|
||||
psz = sz + pad * 2
|
||||
if bg:
|
||||
img = Image.new("RGB", (psz, psz), bg)
|
||||
else:
|
||||
img = Image.new("RGBA", (psz, psz), (0, 0, 0, 0))
|
||||
fg = (fg[0], fg[1], fg[2], 255)
|
||||
for y in range(sz):
|
||||
for x in range(sz):
|
||||
if tab[y][x]:
|
||||
img.putpixel((x + pad, y + pad), fg)
|
||||
if zoom != 1:
|
||||
img = img.resize((sz * zoom, sz * zoom), Image.Resampling.NEAREST)
|
||||
img.save(ap)
|
||||
|
||||
|
||||
def qr2svg(qr: QrCode, border: int) -> str:
|
||||
parts: list[str] = []
|
||||
for y in range(qr.size):
|
||||
sy = border + y
|
||||
for x in range(qr.size):
|
||||
if qr.modules[y][x]:
|
||||
parts.append("M%d,%dh1v1h-1z" % (border + x, sy))
|
||||
t = """\
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 {0} {0}" stroke="none">
|
||||
<rect width="100%" height="100%" fill="#F7F7F7"/>
|
||||
<path d="{1}" fill="#111111"/>
|
||||
</svg>
|
||||
"""
|
||||
return t.format(qr.size + border * 2, " ".join(parts))
|
|
@ -246,24 +246,29 @@ class SMB(object):
|
|||
|
||||
ap = absreal(ap)
|
||||
xbu = vfs.flags.get("xbu")
|
||||
if xbu and not runhook(
|
||||
self.nlog,
|
||||
None,
|
||||
self.hub.up2k,
|
||||
"xbu.smb",
|
||||
xbu,
|
||||
ap,
|
||||
vpath,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
0,
|
||||
0,
|
||||
"1.7.6.2",
|
||||
time.time(),
|
||||
"",
|
||||
):
|
||||
yeet("blocked by xbu server config: %r" % (vpath,))
|
||||
if xbu:
|
||||
hr = runhook(
|
||||
self.nlog,
|
||||
None,
|
||||
self.hub.up2k,
|
||||
"xbu.smb",
|
||||
xbu,
|
||||
ap,
|
||||
vpath,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
0,
|
||||
0,
|
||||
"1.7.6.2",
|
||||
time.time(),
|
||||
"",
|
||||
)
|
||||
t = hr.get("rejectmsg") or ""
|
||||
if t or not hr:
|
||||
if not t:
|
||||
t = "blocked by xbu server config: %r" % (vpath,)
|
||||
yeet(t)
|
||||
|
||||
ret = bos.open(ap, flags, *a, mode=chmod, **ka)
|
||||
if wr:
|
||||
|
@ -318,7 +323,7 @@ class SMB(object):
|
|||
t = "blocked rename (no-move-acc %s): /%s @%s"
|
||||
yeet(t % (vfs1.axs.umove, vp1, uname))
|
||||
|
||||
self.hub.up2k.handle_mv(uname, "1.7.6.2", vp1, vp2)
|
||||
self.hub.up2k.handle_mv("", uname, "1.7.6.2", vp1, vp2)
|
||||
try:
|
||||
bos.makedirs(ap2, vf=vfs2.flags)
|
||||
except:
|
||||
|
@ -373,7 +378,7 @@ class SMB(object):
|
|||
t = "blocked utime (no-write-acc %s): /%s @%s"
|
||||
yeet(t % (vfs.axs.uwrite, vpath, uname))
|
||||
|
||||
return bos.utime(ap, times)
|
||||
bos.utime_c(info, ap, int(times[1]), False)
|
||||
|
||||
def _p_exists(self, vpath: str) -> bool:
|
||||
# ap = "?"
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
# https://github.com/nayuki/QR-Code-generator/blob/daa3114/python/qrcodegen.py
|
||||
# the original ^ is extremely well commented so refer to that for explanations
|
||||
|
||||
# hacks: binary-only, auto-ecc, render, py2-compat
|
||||
# hacks: binary-only, auto-ecc, py2-compat
|
||||
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
|
@ -173,33 +173,6 @@ class QrCode(object):
|
|||
self._apply_mask(msk) # Apply the final choice of mask
|
||||
self._draw_format_bits(msk) # Overwrite old format bits
|
||||
|
||||
def render(self, zoom=1, pad=4) -> str:
|
||||
tab = self.modules
|
||||
sz = self.size
|
||||
if sz % 2 and zoom == 1:
|
||||
tab.append([False] * sz)
|
||||
|
||||
tab = [[False] * sz] * pad + tab + [[False] * sz] * pad
|
||||
tab = [[False] * pad + x + [False] * pad for x in tab]
|
||||
|
||||
rows: list[str] = []
|
||||
if zoom == 1:
|
||||
for y in range(0, len(tab), 2):
|
||||
row = ""
|
||||
for x in range(len(tab[y])):
|
||||
v = 2 if tab[y][x] else 0
|
||||
v += 1 if tab[y + 1][x] else 0
|
||||
row += " ▄▀█"[v]
|
||||
rows.append(row)
|
||||
else:
|
||||
for tr in tab:
|
||||
row = ""
|
||||
for zb in tr:
|
||||
row += " █"[int(zb)] * 2
|
||||
rows.append(row)
|
||||
|
||||
return "\n".join(rows)
|
||||
|
||||
def _draw_function_patterns(self) -> None:
|
||||
# Draw horizontal and vertical timing patterns
|
||||
for i in range(self.size):
|
||||
|
@ -594,20 +567,3 @@ def _get_bit(x: int, i: int) -> bool:
|
|||
|
||||
class DataTooLongError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def qr2svg(qr: QrCode, border: int) -> str:
|
||||
parts: list[str] = []
|
||||
for y in range(qr.size):
|
||||
sy = border + y
|
||||
for x in range(qr.size):
|
||||
if qr.modules[y][x]:
|
||||
parts.append("M%d,%dh1v1h-1z" % (border + x, sy))
|
||||
t = """\
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 {0} {0}" stroke="none">
|
||||
<rect width="100%" height="100%" fill="#F7F7F7"/>
|
||||
<path d="{1}" fill="#111111"/>
|
||||
</svg>
|
||||
"""
|
||||
return t.format(qr.size + border * 2, " ".join(parts))
|
||||
|
|
|
@ -27,9 +27,10 @@ if True: # pylint: disable=using-constant-test
|
|||
from typing import Any, Optional, Union
|
||||
|
||||
from .__init__ import ANYWIN, EXE, MACOS, PY2, TYPE_CHECKING, E, EnvParams, unicode
|
||||
from .authsrv import BAD_CFG, AuthSrv
|
||||
from .authsrv import BAD_CFG, AuthSrv, derive_args, n_du_who, n_ver_who
|
||||
from .bos import bos
|
||||
from .cert import ensure_cert
|
||||
from .fsutil import ramdisk_chk
|
||||
from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, HAVE_MUTAGEN
|
||||
from .pwhash import HAVE_ARGON2
|
||||
from .tcpsrv import TcpSrv
|
||||
|
@ -66,6 +67,7 @@ from .util import (
|
|||
build_netmap,
|
||||
expat_ver,
|
||||
gzip,
|
||||
html_escape,
|
||||
load_ipr,
|
||||
load_ipu,
|
||||
lock_file,
|
||||
|
@ -134,6 +136,7 @@ class SvcHub(object):
|
|||
self.nsigs = 3
|
||||
self.retcode = 0
|
||||
self.httpsrv_up = 0
|
||||
self.qr_tsz = None
|
||||
|
||||
self.log_mutex = threading.Lock()
|
||||
self.cday = 0
|
||||
|
@ -155,7 +158,7 @@ class SvcHub(object):
|
|||
args.unpost = 0
|
||||
args.no_del = True
|
||||
args.no_mv = True
|
||||
args.hardlink = True
|
||||
args.reflink = True
|
||||
args.dav_auth = True
|
||||
args.vague_403 = True
|
||||
args.nih = True
|
||||
|
@ -245,8 +248,8 @@ class SvcHub(object):
|
|||
t = "WARNING: --th-ram-max is very small (%.2f GiB); will not be able to %s"
|
||||
self.log("root", t % (args.th_ram_max, zs), 3)
|
||||
|
||||
if args.chpw and args.have_idp_hdrs:
|
||||
t = "ERROR: user-changeable passwords is incompatible with IdP/identity-providers; you must disable either --chpw or --idp-h-usr"
|
||||
if args.chpw and args.have_idp_hdrs and "pw" not in args.auth_ord.split(","):
|
||||
t = "ERROR: user-changeable passwords is not compatible with your current configuration. Choose one of these options to fix it:\n option1: disable --chpw\n option2: remove all use of IdP features; --idp-*\n option3: change --auth-ord to something like pw,idp,ipu"
|
||||
self.log("root", t, 1)
|
||||
raise Exception(t)
|
||||
|
||||
|
@ -288,6 +291,14 @@ class SvcHub(object):
|
|||
ch = "abcdefghijklmnopqrstuvwx"[int(args.theme / 2)]
|
||||
args.theme = "{0}{1} {0} {1}".format(ch, bri)
|
||||
|
||||
if args.nid:
|
||||
args.du_who = "no"
|
||||
args.du_iwho = n_du_who(args.du_who)
|
||||
|
||||
if args.ver and args.ver_who == "no":
|
||||
args.ver_who = "all"
|
||||
args.ver_iwho = n_ver_who(args.ver_who)
|
||||
|
||||
if args.nih:
|
||||
args.vname = ""
|
||||
args.doctitle = args.doctitle.replace(" @ --name", "")
|
||||
|
@ -301,6 +312,7 @@ class SvcHub(object):
|
|||
|
||||
# initiate all services to manage
|
||||
self.asrv = AuthSrv(self.args, self.log, dargs=self.dargs)
|
||||
ramdisk_chk(self.asrv)
|
||||
|
||||
if args.cgen:
|
||||
self.asrv.cgen()
|
||||
|
@ -325,7 +337,7 @@ class SvcHub(object):
|
|||
|
||||
self._feature_test()
|
||||
|
||||
decs = {k: 1 for k in self.args.th_dec.split(",")}
|
||||
decs = {k.strip(): 1 for k in self.args.th_dec.split(",")}
|
||||
if not HAVE_VIPS:
|
||||
decs.pop("vips", None)
|
||||
if not HAVE_PIL:
|
||||
|
@ -380,7 +392,10 @@ class SvcHub(object):
|
|||
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 = {}
|
||||
zss = set(args.th_r_ffa.split(",") + args.th_r_ffv.split(","))
|
||||
args.au_unpk = {
|
||||
k: v for k, v in args.au_unpk.items() if v.split(".")[0] not in zss
|
||||
}
|
||||
|
||||
args.th_poke = min(args.th_poke, args.th_maxage, args.ac_maxage)
|
||||
|
||||
|
@ -433,7 +448,7 @@ class SvcHub(object):
|
|||
|
||||
# create netmaps early to avoid firewall gaps,
|
||||
# but the mutex blocks multiprocessing startup
|
||||
for zs in "ipu_iu ftp_ipa_nm tftp_ipa_nm".split():
|
||||
for zs in "ipu_nm ftp_ipa_nm tftp_ipa_nm".split():
|
||||
try:
|
||||
getattr(args, zs).mutex = threading.Lock()
|
||||
except:
|
||||
|
@ -787,7 +802,27 @@ class SvcHub(object):
|
|||
self.signal_handler(signal.SIGTERM, None)
|
||||
|
||||
def sticky_qr(self) -> None:
|
||||
tw, th = termsize()
|
||||
self._sticky_qr()
|
||||
|
||||
def _unsticky_qr(self, flush=True) -> None:
|
||||
print("\033[s\033[J\033[r\033[u", file=sys.stderr, end="")
|
||||
if flush:
|
||||
sys.stderr.flush()
|
||||
|
||||
def _sticky_qr(self, force: bool = False) -> None:
|
||||
sz = termsize()
|
||||
if self.qr_tsz == sz:
|
||||
if not force:
|
||||
return
|
||||
else:
|
||||
force = False
|
||||
|
||||
if self.qr_tsz:
|
||||
self._unsticky_qr(False)
|
||||
else:
|
||||
atexit.register(self._unsticky_qr)
|
||||
|
||||
tw, th = self.qr_tsz = sz
|
||||
zs1, qr = self.tcpsrv.qr.split("\n", 1)
|
||||
url, colr = zs1.split(" ", 1)
|
||||
nl = len(qr.split("\n")) # numlines
|
||||
|
@ -811,13 +846,38 @@ class SvcHub(object):
|
|||
url = "%s\033[%d;%dH%s\033[0m" % (colr, sh + 1, (nl + lp) * 2, url)
|
||||
qr = colr + qr
|
||||
|
||||
def unlock():
|
||||
print("\033[s\033[r\033[u", file=sys.stderr)
|
||||
|
||||
atexit.register(unlock)
|
||||
t = "%s\033[%dA" % ("\n" * nl, nl)
|
||||
t = "%s\033[s\033[1;%dr\033[%dH%s%s\033[u" % (t, sh - 1, sh, qr, url)
|
||||
self.pr(t, file=sys.stderr)
|
||||
if not force:
|
||||
self.log("qr", "sticky-qrcode %sx%s,%s" % (tw, th, sh), 6)
|
||||
self.pr(t, file=sys.stderr, end="")
|
||||
|
||||
def _qr_thr(self):
|
||||
qr = self.tcpsrv.qr
|
||||
w8 = self.args.qr_wait
|
||||
if w8:
|
||||
time.sleep(w8)
|
||||
self.log("qr-code", qr)
|
||||
if self.args.qr_stdout:
|
||||
self.pr(self.tcpsrv.qr)
|
||||
if self.args.qr_stderr:
|
||||
self.pr(self.tcpsrv.qr, file=sys.stderr)
|
||||
w8 = self.args.qr_every
|
||||
msg = "%s\033[%dA" % (qr, len(qr.split("\n")))
|
||||
while w8:
|
||||
time.sleep(w8)
|
||||
if self.stopping:
|
||||
break
|
||||
if self.args.qr_pin:
|
||||
self._sticky_qr(True)
|
||||
else:
|
||||
self.log("qr-code", msg)
|
||||
w8 = self.args.qr_winch
|
||||
while w8:
|
||||
time.sleep(w8)
|
||||
if self.stopping:
|
||||
break
|
||||
self._sticky_qr()
|
||||
|
||||
def cb_httpsrv_up(self) -> None:
|
||||
self.httpsrv_up += 1
|
||||
|
@ -833,8 +893,15 @@ class SvcHub(object):
|
|||
if self.tcpsrv.qr:
|
||||
if self.args.qr_pin:
|
||||
self.sticky_qr()
|
||||
if self.args.qr_wait or self.args.qr_every or self.args.qr_winch:
|
||||
Daemon(self._qr_thr, "qr")
|
||||
else:
|
||||
self.log("qr-code", self.tcpsrv.qr)
|
||||
if not self.args.qr_pin:
|
||||
self.log("qr-code", self.tcpsrv.qr)
|
||||
if self.args.qr_stdout:
|
||||
self.pr(self.tcpsrv.qr)
|
||||
if self.args.qr_stderr:
|
||||
self.pr(self.tcpsrv.qr, file=sys.stderr)
|
||||
else:
|
||||
self.log("root", "workers OK\n")
|
||||
|
||||
|
@ -932,6 +999,24 @@ class SvcHub(object):
|
|||
t = "WARNING:\nDisabling WebDAV support because dxml selftest failed. Please report this bug;\n%s\n...and include the following information in the bug-report:\n%s | expat %s\n"
|
||||
self.log("root", t % (URL_BUG, VERSIONS, expat_ver()), 1)
|
||||
|
||||
if not E.scfg and not al.unsafe_state and not os.getenv("PRTY_UNSAFE_STATE"):
|
||||
t = "because runtime config is currently being stored in an untrusted emergency-fallback location. Please fix your environment so either XDG_CONFIG_HOME or ~/.config can be used instead, or disable this safeguard with --unsafe-state or env-var PRTY_UNSAFE_STATE=1."
|
||||
if not al.no_ses:
|
||||
al.no_ses = True
|
||||
t2 = "A consequence of this misconfiguration is that passwords will now be sent in the HTTP-header of every request!"
|
||||
self.log("root", "WARNING:\nWill disable sessions %s %s" % (t, t2), 1)
|
||||
if al.idp_store == 1:
|
||||
al.idp_store = 0
|
||||
self.log("root", "WARNING:\nDisabling --idp-store %s" % (t,), 3)
|
||||
if al.idp_store:
|
||||
t2 = "ERROR: Cannot enable --idp-store %s" % (t,)
|
||||
self.log("root", t2, 1)
|
||||
raise Exception(t2)
|
||||
if al.shr:
|
||||
t2 = "ERROR: Cannot enable shares %s" % (t,)
|
||||
self.log("root", t2, 1)
|
||||
raise Exception(t2)
|
||||
|
||||
def _process_config(self) -> bool:
|
||||
al = self.args
|
||||
|
||||
|
@ -1087,8 +1172,15 @@ class SvcHub(object):
|
|||
if len(al.tcolor) == 3: # fc5 => ffcc55
|
||||
al.tcolor = "".join([x * 2 for x in al.tcolor])
|
||||
|
||||
if self.args.name_url:
|
||||
zs = html_escape(self.args.name_url, True, True)
|
||||
zs = '<a href="%s">%s</a>' % (zs, self.args.name)
|
||||
else:
|
||||
zs = self.args.name
|
||||
self.args.name_html = zs
|
||||
|
||||
zs = al.u2sz
|
||||
zsl = zs.split(",")
|
||||
zsl = [x.strip() for x in zs.split(",")]
|
||||
if len(zsl) not in (1, 3):
|
||||
t = "invalid --u2sz; must be either one number, or a comma-separated list of three numbers (min,default,max)"
|
||||
raise Exception(t)
|
||||
|
@ -1105,6 +1197,7 @@ class SvcHub(object):
|
|||
zi2 = zi
|
||||
al.u2sz = ",".join(zsl)
|
||||
|
||||
derive_args(al)
|
||||
return True
|
||||
|
||||
def _ipa2re(self, txt) -> Optional[re.Pattern]:
|
||||
|
@ -1280,6 +1373,7 @@ class SvcHub(object):
|
|||
with self.reload_mutex:
|
||||
self.log("root", "reloading config")
|
||||
self.asrv.reload(9 if up2k else 4)
|
||||
ramdisk_chk(self.asrv)
|
||||
if up2k:
|
||||
self.up2k.reload(rescan_all_vols)
|
||||
t += "; volumes are now reinitializing"
|
||||
|
|
|
@ -9,17 +9,19 @@ import time
|
|||
|
||||
from .__init__ import ANYWIN, PY2, TYPE_CHECKING, unicode
|
||||
from .cert import gencert
|
||||
from .stolen.qrcodegen import QrCode
|
||||
from .qrkode import QrCode, qr2png, qr2svg, qr2txt, qrgen
|
||||
from .util import (
|
||||
E_ACCESS,
|
||||
E_ADDR_IN_USE,
|
||||
E_ADDR_NOT_AVAIL,
|
||||
E_UNREACH,
|
||||
HAVE_IPV6,
|
||||
IP6_LL,
|
||||
IP6ALL,
|
||||
VF_CAREFUL,
|
||||
Netdev,
|
||||
atomic_move,
|
||||
get_adapters,
|
||||
min_ex,
|
||||
sunpack,
|
||||
termsize,
|
||||
|
@ -58,6 +60,7 @@ class TcpSrv(object):
|
|||
self.stopping = False
|
||||
self.srv: list[socket.socket] = []
|
||||
self.bound: list[tuple[str, int]] = []
|
||||
self.seen_eps: list[tuple[str, int]] = [] # also skipped by uds-only
|
||||
self.netdevs: dict[str, Netdev] = {}
|
||||
self.netlist = ""
|
||||
self.nsrv = 0
|
||||
|
@ -140,12 +143,12 @@ class TcpSrv(object):
|
|||
# keep IPv6 LL-only nics
|
||||
ll_ok: set[str] = set()
|
||||
for ip, nd in self.netdevs.items():
|
||||
if not ip.startswith("fe80"):
|
||||
if not ip.startswith(IP6_LL):
|
||||
continue
|
||||
|
||||
just_ll = True
|
||||
for ip2, nd2 in self.netdevs.items():
|
||||
if nd == nd2 and ":" in ip2 and not ip2.startswith("fe80"):
|
||||
if nd == nd2 and ":" in ip2 and not ip2.startswith(IP6_LL):
|
||||
just_ll = False
|
||||
|
||||
if just_ll or self.args.ll:
|
||||
|
@ -164,7 +167,7 @@ class TcpSrv(object):
|
|||
title_vars = [x[1:] for x in self.args.wintitle.split(" ") if x.startswith("$")]
|
||||
t = "available @ {}://{}:{}/ (\033[33m{}\033[0m)"
|
||||
for ip, desc in sorted(eps.items(), key=lambda x: x[1]):
|
||||
if ip.startswith("fe80") and ip not in ll_ok:
|
||||
if ip.startswith(IP6_LL) and ip not in ll_ok:
|
||||
continue
|
||||
|
||||
for port in sorted(self.args.p):
|
||||
|
@ -298,12 +301,19 @@ class TcpSrv(object):
|
|||
|
||||
try:
|
||||
if tcp:
|
||||
if self.args.http_no_tcp:
|
||||
self.seen_eps.append((ip, port))
|
||||
return
|
||||
srv.bind((ip, port))
|
||||
else:
|
||||
if ANYWIN or self.args.rm_sck:
|
||||
if os.path.exists(ip):
|
||||
os.unlink(ip)
|
||||
srv.bind(ip)
|
||||
if uds_gid != -1:
|
||||
os.chown(ip, -1, uds_gid)
|
||||
if uds_perm != -1:
|
||||
os.chmod(ip, uds_perm)
|
||||
else:
|
||||
tf = "%s.%d" % (ip, os.getpid())
|
||||
if os.path.exists(tf):
|
||||
|
@ -404,6 +414,7 @@ class TcpSrv(object):
|
|||
|
||||
self.srv = srvs
|
||||
self.bound = bound
|
||||
self.seen_eps = list(set(self.seen_eps + bound))
|
||||
self.nsrv = len(srvs)
|
||||
self._distribute_netdevs()
|
||||
|
||||
|
@ -446,8 +457,6 @@ class TcpSrv(object):
|
|||
self._distribute_netdevs()
|
||||
|
||||
def detect_interfaces(self, listen_ips: list[str]) -> dict[str, Netdev]:
|
||||
from .stolen.ifaddr import get_adapters
|
||||
|
||||
listen_ips = [x for x in listen_ips if not x.startswith(("unix:", "fd:"))]
|
||||
|
||||
nics = get_adapters(True)
|
||||
|
@ -620,7 +629,11 @@ class TcpSrv(object):
|
|||
|
||||
pad = self.args.qrp
|
||||
zoom = self.args.qrz
|
||||
qrc = QrCode.encode_binary(btxt)
|
||||
qrc = qrgen(btxt)
|
||||
|
||||
for zs in self.args.qr_file or []:
|
||||
self._qr2file(qrc, zs)
|
||||
|
||||
if zoom == 0:
|
||||
try:
|
||||
tw, th = termsize()
|
||||
|
@ -629,13 +642,15 @@ class TcpSrv(object):
|
|||
except:
|
||||
zoom = 1
|
||||
|
||||
qr = qrc.render(zoom, pad)
|
||||
qr = qr2txt(qrc, zoom, pad)
|
||||
if self.args.no_ansi:
|
||||
return "{}\n{}".format(txt, qr)
|
||||
|
||||
halfc = "\033[40;48;5;{0}m{1}\033[47;48;5;{2}m"
|
||||
if not fg:
|
||||
halfc = "\033[0;40m{1}\033[0;47m"
|
||||
if nocolor:
|
||||
halfc = "\033[0;7m{1}\033[0m"
|
||||
|
||||
def ansify(m: re.Match) -> str:
|
||||
return halfc.format(fg, " " * len(m.group(1)), bg)
|
||||
|
@ -654,3 +669,29 @@ class TcpSrv(object):
|
|||
t = t.replace("\n", "`\n`")
|
||||
|
||||
return txt + t
|
||||
|
||||
def _qr2file(self, qrc: QrCode, txt: str):
|
||||
if ".txt:" in txt or ".svg:" in txt:
|
||||
ap, zs1, zs2 = txt.rsplit(":", 2)
|
||||
bg = fg = ""
|
||||
else:
|
||||
ap, zs1, zs2, bg, fg = txt.rsplit(":", 4)
|
||||
zoom = int(zs1)
|
||||
pad = int(zs2)
|
||||
|
||||
if ap.endswith(".txt"):
|
||||
if zoom not in (1, 2):
|
||||
raise Exception("invalid zoom for qr.txt; must be 1 or 2")
|
||||
with open(ap, "wb") as f:
|
||||
f.write(qr2txt(qrc, zoom, pad).encode("utf-8"))
|
||||
elif ap.endswith(".svg"):
|
||||
with open(ap, "wb") as f:
|
||||
f.write(qr2svg(qrc, pad).encode("utf-8"))
|
||||
else:
|
||||
qr2png(qrc, zoom, pad, self._h2i(bg), self._h2i(fg), ap)
|
||||
|
||||
def _h2i(self, hs):
|
||||
try:
|
||||
return tuple(int(hs[i : i + 2], 16) for i in (0, 2, 4))
|
||||
except:
|
||||
return None
|
||||
|
|
|
@ -363,24 +363,29 @@ class Tftpd(object):
|
|||
yeet("blocked write; folder not world-deletable: /%s" % (vpath,))
|
||||
|
||||
xbu = vfs.flags.get("xbu")
|
||||
if xbu and not runhook(
|
||||
self.nlog,
|
||||
None,
|
||||
self.hub.up2k,
|
||||
"xbu.tftpd",
|
||||
xbu,
|
||||
ap,
|
||||
vpath,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
0,
|
||||
0,
|
||||
"8.3.8.7",
|
||||
time.time(),
|
||||
"",
|
||||
):
|
||||
yeet("blocked by xbu server config: %r" % (vpath,))
|
||||
if xbu:
|
||||
hr = runhook(
|
||||
self.nlog,
|
||||
None,
|
||||
self.hub.up2k,
|
||||
"xbu.tftpd",
|
||||
xbu,
|
||||
ap,
|
||||
vpath,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
0,
|
||||
0,
|
||||
"8.3.8.7",
|
||||
time.time(),
|
||||
"",
|
||||
)
|
||||
t = hr.get("rejectmsg") or ""
|
||||
if t or not hr:
|
||||
if not t:
|
||||
t = "upload blocked by xbu server config: %r" % (vpath,)
|
||||
yeet(t)
|
||||
|
||||
if not self.args.tftp_nols and bos.path.isdir(ap):
|
||||
return self._ls(vpath, "", 0, True)
|
||||
|
|
|
@ -381,7 +381,7 @@ class ThumbSrv(object):
|
|||
else:
|
||||
ap_unpk = abspath
|
||||
|
||||
if not bos.path.exists(tpath):
|
||||
if ap_unpk and not bos.path.exists(tpath):
|
||||
tex = tpath.rsplit(".", 1)[-1]
|
||||
want_mp3 = tex == "mp3"
|
||||
want_opus = tex in ("opus", "owa", "caf")
|
||||
|
@ -424,12 +424,14 @@ class ThumbSrv(object):
|
|||
except:
|
||||
pass
|
||||
|
||||
conv_ok = False
|
||||
for fun in funs:
|
||||
try:
|
||||
if not png_ok and tpath.endswith(".png"):
|
||||
raise Exception("png only allowed for waveforms")
|
||||
|
||||
fun(ap_unpk, ttpath, fmt, vn)
|
||||
conv_ok = True
|
||||
break
|
||||
except Exception as ex:
|
||||
msg = "%s could not create thumbnail of %r\n%s"
|
||||
|
@ -451,16 +453,20 @@ class ThumbSrv(object):
|
|||
except:
|
||||
pass
|
||||
|
||||
if abspath != ap_unpk:
|
||||
if abspath != ap_unpk and ap_unpk:
|
||||
wunlink(self.log, ap_unpk, vn.flags)
|
||||
|
||||
try:
|
||||
atomic_move(self.log, ttpath, tpath, vn.flags)
|
||||
except Exception as ex:
|
||||
if not os.path.exists(tpath):
|
||||
if conv_ok and not os.path.exists(tpath):
|
||||
t = "failed to move [%s] to [%s]: %r"
|
||||
self.log(t % (ttpath, tpath, ex), 3)
|
||||
pass
|
||||
elif not conv_ok:
|
||||
try:
|
||||
open(tpath, "ab").close()
|
||||
except:
|
||||
pass
|
||||
|
||||
untemp = []
|
||||
with self.mutex:
|
||||
|
@ -682,7 +688,7 @@ class ThumbSrv(object):
|
|||
return
|
||||
|
||||
c: Union[str, int] = "90"
|
||||
t = "FFmpeg failed (probably a corrupt video file):\n"
|
||||
t = "FFmpeg failed (probably a corrupt file):\n"
|
||||
if (
|
||||
(not self.args.th_ff_jpg or time.time() - int(self.args.th_ff_jpg) < 60)
|
||||
and cmd[-1].lower().endswith(b".webp")
|
||||
|
|
|
@ -53,6 +53,11 @@ class U2idx(object):
|
|||
self.log("your python does not have sqlite3; searching will be disabled")
|
||||
return
|
||||
|
||||
if self.args.srch_icase:
|
||||
self._open_db = self._open_db_icase
|
||||
else:
|
||||
self._open_db = self._open_db_std
|
||||
|
||||
assert sqlite3 # type: ignore # !rm
|
||||
|
||||
self.active_id = ""
|
||||
|
@ -69,6 +74,16 @@ class U2idx(object):
|
|||
def log(self, msg: str, c: Union[int, str] = 0) -> None:
|
||||
self.log_func("u2idx", msg, c)
|
||||
|
||||
def _open_db_std(self, *args, **kwargs):
|
||||
assert sqlite3 # type: ignore # !rm
|
||||
kwargs["check_same_thread"] = False
|
||||
return sqlite3.connect(*args, **kwargs)
|
||||
|
||||
def _open_db_icase(self, *args, **kwargs):
|
||||
db = self._open_db_std(*args, **kwargs)
|
||||
db.create_function("casefold", 1, lambda x: x.casefold() if x else x)
|
||||
return db
|
||||
|
||||
def shutdown(self) -> None:
|
||||
if not HAVE_SQLITE3:
|
||||
return
|
||||
|
@ -148,8 +163,7 @@ class U2idx(object):
|
|||
uri = ""
|
||||
try:
|
||||
uri = "{}?mode=ro&nolock=1".format(Path(db_path).as_uri())
|
||||
db = sqlite3.connect(uri, timeout=2, uri=True, check_same_thread=False)
|
||||
cur = db.cursor()
|
||||
cur = self._open_db(uri, timeout=2, uri=True).cursor()
|
||||
cur.execute('pragma table_info("up")').fetchone()
|
||||
self.log("ro: %r" % (db_path,))
|
||||
except:
|
||||
|
@ -160,7 +174,7 @@ class U2idx(object):
|
|||
if not cur:
|
||||
# on windows, this steals the write-lock from up2k.deferred_init --
|
||||
# seen on win 10.0.17763.2686, py 3.10.4, sqlite 3.37.2
|
||||
cur = sqlite3.connect(db_path, timeout=2, check_same_thread=False).cursor()
|
||||
cur = self._open_db(db_path, timeout=2).cursor()
|
||||
self.log("opened %r" % (db_path,))
|
||||
|
||||
self.cur[ptop] = cur
|
||||
|
@ -173,6 +187,8 @@ class U2idx(object):
|
|||
if not HAVE_SQLITE3:
|
||||
return [], [], False
|
||||
|
||||
icase = self.args.srch_icase
|
||||
|
||||
q = ""
|
||||
v: Union[str, int] = ""
|
||||
va: list[Union[str, int]] = []
|
||||
|
@ -180,6 +196,7 @@ class U2idx(object):
|
|||
is_key = True
|
||||
is_size = False
|
||||
is_date = False
|
||||
is_wark = False
|
||||
field_end = "" # closing parenthesis or whatever
|
||||
kw_key = ["(", ")", "and ", "or ", "not "]
|
||||
kw_val = ["==", "=", "!=", ">", ">=", "<", "<=", "like "]
|
||||
|
@ -198,6 +215,8 @@ class U2idx(object):
|
|||
is_key = kw in kw_key
|
||||
uq = uq[len(kw) :]
|
||||
ok = True
|
||||
if is_wark:
|
||||
kw = "= "
|
||||
q += kw
|
||||
break
|
||||
|
||||
|
@ -232,9 +251,17 @@ class U2idx(object):
|
|||
elif v == "path":
|
||||
v = "trim(?||up.rd,'/')"
|
||||
va.append("\nrd")
|
||||
if icase:
|
||||
v = "casefold(%s)" % (v,)
|
||||
|
||||
elif v == "name":
|
||||
v = "up.fn"
|
||||
if icase:
|
||||
v = "casefold(%s)" % (v,)
|
||||
|
||||
elif v == "w":
|
||||
v = "substr(up.w,1,16)"
|
||||
is_wark = True
|
||||
|
||||
elif v == "tags" or ptn_mt.match(v):
|
||||
have_mt = True
|
||||
|
@ -247,7 +274,7 @@ class U2idx(object):
|
|||
v = "exists(select 1 from mt where mt.w = mtw and " + vq
|
||||
|
||||
else:
|
||||
raise Pebkac(400, "invalid key [{}]".format(v))
|
||||
raise Pebkac(400, "invalid key %r" % (v,))
|
||||
|
||||
q += v + " "
|
||||
continue
|
||||
|
@ -276,6 +303,14 @@ class U2idx(object):
|
|||
is_size = False
|
||||
v = int(float(v) * 1024 * 1024)
|
||||
|
||||
elif is_wark:
|
||||
is_wark = False
|
||||
v = v.strip("*")
|
||||
if len(v) > 16:
|
||||
v = v[:16]
|
||||
if len(v) < 16:
|
||||
raise Pebkac(400, "w/filehash must be 16+ chars")
|
||||
|
||||
else:
|
||||
if v.startswith("*"):
|
||||
head = "'%'||"
|
||||
|
@ -285,6 +320,12 @@ class U2idx(object):
|
|||
tail = "||'%'"
|
||||
v = v[:-1]
|
||||
|
||||
if icase and "casefold(" in q:
|
||||
try:
|
||||
v = unicode(v).casefold()
|
||||
except:
|
||||
v = unicode(v).lower()
|
||||
|
||||
q += " {}?{} ".format(head, tail)
|
||||
va.append(v)
|
||||
is_key = True
|
||||
|
@ -319,7 +360,7 @@ class U2idx(object):
|
|||
uname: str,
|
||||
vols: list[VFS],
|
||||
uq: str,
|
||||
uv: list[Union[str, int]],
|
||||
uv: Union[list[str], list[Union[str, int]]],
|
||||
have_mt: bool,
|
||||
sort: bool,
|
||||
lim: int,
|
||||
|
|
|
@ -18,13 +18,15 @@ from copy import deepcopy
|
|||
|
||||
from queue import Queue
|
||||
|
||||
from .__init__ import ANYWIN, PY2, TYPE_CHECKING, WINDOWS, E
|
||||
from .__init__ import ANYWIN, MACOS, PY2, TYPE_CHECKING, WINDOWS, E
|
||||
from .authsrv import LEELOO_DALLAS, SEESLOG, VFS, AuthSrv
|
||||
from .bos import bos
|
||||
from .cfg import vf_bmap, vf_cmap, vf_vmap
|
||||
from .fsutil import Fstab
|
||||
from .mtag import MParser, MTag
|
||||
from .util import (
|
||||
E_FS_CRIT,
|
||||
E_FS_MEH,
|
||||
HAVE_SQLITE3,
|
||||
SYMTIME,
|
||||
VF_CAREFUL,
|
||||
|
@ -60,6 +62,7 @@ from .util import (
|
|||
sfsenc,
|
||||
spack,
|
||||
statdir,
|
||||
trystat_shutil_copy2,
|
||||
ub64enc,
|
||||
unhumanize,
|
||||
vjoin,
|
||||
|
@ -91,6 +94,9 @@ ICV_EXTS = set(zsg.split(","))
|
|||
zsg = "3gp,asf,av1,avc,avi,flv,m4v,mjpeg,mjpg,mkv,mov,mp4,mpeg,mpeg2,mpegts,mpg,mpg2,mts,nut,ogm,ogv,rm,vob,webm,wmv"
|
||||
VCV_EXTS = set(zsg.split(","))
|
||||
|
||||
zsg = "aif,aiff,alac,ape,flac,m4a,mp3,oga,ogg,opus,tak,tta,wav,wma,wv,cbz,epub"
|
||||
ACV_EXTS = set(zsg.split(","))
|
||||
|
||||
zsg = "nohash noidx xdev xvol"
|
||||
VF_AFFECTS_INDEXING = set(zsg.split(" "))
|
||||
|
||||
|
@ -209,7 +215,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.args)
|
||||
self.fstab = Fstab(self.log_func, self.args, True)
|
||||
self.gen_fk = self._gen_fk if self.args.log_fk else gen_filekey
|
||||
|
||||
if self.args.hash_mt < 2:
|
||||
|
@ -411,10 +417,11 @@ class Up2k(object):
|
|||
|
||||
ret: list[tuple[int, str, int, int, int]] = []
|
||||
userset = set([(uname or "\n"), "*"])
|
||||
e_d = {}
|
||||
n = 1000
|
||||
try:
|
||||
for ptop, tab2 in self.registry.items():
|
||||
cfg = self.flags.get(ptop, {}).get("u2abort", 1)
|
||||
cfg = self.flags.get(ptop, e_d).get("u2abort", 1)
|
||||
if not cfg:
|
||||
continue
|
||||
addr = (ip or "\n") if cfg in (1, 2) else ""
|
||||
|
@ -923,6 +930,12 @@ class Up2k(object):
|
|||
with self.mutex, self.reg_mutex:
|
||||
# only need to protect register_vpath but all in one go feels right
|
||||
for vol in vols:
|
||||
if bos.path.isfile(vol.realpath):
|
||||
self.volstate[vol.vpath] = "online (just-a-file)"
|
||||
t = "NOTE: volume [/%s] is a file, not a folder"
|
||||
self.log(t % (vol.vpath,))
|
||||
continue
|
||||
|
||||
try:
|
||||
# mkdir gonna happen at snap anyways;
|
||||
bos.makedirs(vol.realpath, vf=vol.flags)
|
||||
|
@ -1129,7 +1142,7 @@ class Up2k(object):
|
|||
ft = "\033[0;32m{}{:.0}"
|
||||
ff = "\033[0;35m{}{:.0}"
|
||||
fv = "\033[0;36m{}:\033[90m{}"
|
||||
zs = "ext_th_d html_head put_name2 mv_re_r mv_re_t rm_re_r rm_re_t srch_re_dots srch_re_nodot zipmax zipmaxn_v zipmaxs_v"
|
||||
zs = "bcasechk du_iwho ext_th_d html_head html_head_d html_head_s put_name2 mv_re_r mv_re_t rm_re_r rm_re_t srch_re_dots srch_re_nodot zipmax zipmaxn_v zipmaxs_v"
|
||||
fx = set(zs.split())
|
||||
fd = vf_bmap()
|
||||
fd.update(vf_cmap())
|
||||
|
@ -1483,10 +1496,12 @@ class Up2k(object):
|
|||
unreg: list[str] = []
|
||||
files: list[tuple[int, int, str]] = []
|
||||
fat32 = True
|
||||
cv = vcv = ""
|
||||
cv = vcv = acv = ""
|
||||
e_d = {}
|
||||
|
||||
th_cvd = self.args.th_coversd
|
||||
th_cvds = self.args.th_coversd_set
|
||||
scan_pr_s = self.args.scan_pr_s
|
||||
|
||||
assert self.pp and self.mem_cur # !rm
|
||||
self.pp.msg = "a%d %s" % (self.pp.n, cdir)
|
||||
|
@ -1593,9 +1608,11 @@ class Up2k(object):
|
|||
cv = iname
|
||||
elif not vcv and ext in VCV_EXTS and not iname.startswith("."):
|
||||
vcv = iname
|
||||
elif not acv and ext in ACV_EXTS and not iname.startswith("."):
|
||||
acv = iname
|
||||
|
||||
if not cv:
|
||||
cv = vcv
|
||||
cv = vcv or acv
|
||||
|
||||
if not self.args.no_dirsz:
|
||||
tnf += len(files)
|
||||
|
@ -1682,7 +1699,7 @@ class Up2k(object):
|
|||
|
||||
t = "reindex %r => %r mtime(%s/%s) size(%s/%s)"
|
||||
self.log(t % (top, rp, dts, lmod, dsz, sz))
|
||||
self.db_rm(db.c, rd, fn, 0)
|
||||
self.db_rm(db.c, e_d, rd, fn, 0)
|
||||
tfa += 1
|
||||
db.n += 1
|
||||
in_db = []
|
||||
|
@ -1697,7 +1714,7 @@ class Up2k(object):
|
|||
if nohash or not sz:
|
||||
wark = up2k_wark_from_metadata(self.salt, sz, lmod, rd, fn)
|
||||
else:
|
||||
if sz > 1024 * 1024:
|
||||
if sz > 1024 * 1024 * scan_pr_s:
|
||||
self.log("file: %r" % (abspath,))
|
||||
|
||||
try:
|
||||
|
@ -1705,7 +1722,7 @@ class Up2k(object):
|
|||
abspath, "a{}, ".format(self.pp.n)
|
||||
)
|
||||
except Exception as ex:
|
||||
self.log("hash: %r @ %r" % (ex, abspath))
|
||||
self._ex_hash(ex, abspath)
|
||||
continue
|
||||
|
||||
if not hashes:
|
||||
|
@ -1719,7 +1736,7 @@ class Up2k(object):
|
|||
un = ""
|
||||
|
||||
# skip upload hooks by not providing vflags
|
||||
self.db_add(db.c, {}, rd, fn, lmod, sz, "", "", wark, wark, "", un, ip, at)
|
||||
self.db_add(db.c, e_d, rd, fn, lmod, sz, "", "", wark, wark, "", un, ip, at)
|
||||
db.n += 1
|
||||
db.nf += 1
|
||||
tfa += 1
|
||||
|
@ -1779,7 +1796,7 @@ class Up2k(object):
|
|||
rm_files = [x for x in hits if x not in seen_files]
|
||||
n_rm = len(rm_files)
|
||||
for fn in rm_files:
|
||||
self.db_rm(db.c, rd, fn, 0)
|
||||
self.db_rm(db.c, e_d, rd, fn, 0)
|
||||
|
||||
if n_rm:
|
||||
self.log("forgot {} deleted files".format(n_rm))
|
||||
|
@ -1966,7 +1983,7 @@ class Up2k(object):
|
|||
try:
|
||||
hashes, _ = self._hashlist_from_file(abspath, pf)
|
||||
except Exception as ex:
|
||||
self.log("hash: %r @ %r" % (ex, abspath))
|
||||
self._ex_hash(ex, abspath)
|
||||
continue
|
||||
|
||||
if not hashes:
|
||||
|
@ -2755,7 +2772,7 @@ class Up2k(object):
|
|||
cur.close()
|
||||
db.close()
|
||||
|
||||
shutil.copy2(fsenc(db_path), fsenc(bak))
|
||||
trystat_shutil_copy2(self.log, fsenc(db_path), fsenc(bak))
|
||||
return self._orz(db_path)
|
||||
|
||||
def _read_ver(self, cur: "sqlite3.Cursor") -> Optional[int]:
|
||||
|
@ -2992,7 +3009,7 @@ class Up2k(object):
|
|||
|
||||
# check if filesystem supports sparse files;
|
||||
# refuse out-of-order / multithreaded uploading if sprs False
|
||||
sprs = self.fstab.get(pdir) != "ng"
|
||||
sprs = self.fstab.get(pdir)[0] != "ng"
|
||||
|
||||
if True:
|
||||
jcur = self.cur.get(ptop)
|
||||
|
@ -3130,7 +3147,7 @@ class Up2k(object):
|
|||
for cur, dp_dir, dp_fn in lost:
|
||||
t = "forgetting desynced db entry: %r"
|
||||
self.log(t % ("/" + vjoin(vjoin(vfs.vpath, dp_dir), dp_fn)))
|
||||
self.db_rm(cur, dp_dir, dp_fn, cj["size"])
|
||||
self.db_rm(cur, vfs.flags, dp_dir, dp_fn, cj["size"])
|
||||
if c2 and c2 != cur:
|
||||
c2.connection.commit()
|
||||
|
||||
|
@ -3283,8 +3300,11 @@ class Up2k(object):
|
|||
job["at"],
|
||||
"",
|
||||
)
|
||||
if not hr:
|
||||
t = "upload blocked by xbu server config: %r" % (dst,)
|
||||
t = hr.get("rejectmsg") or ""
|
||||
if t or not hr:
|
||||
if not t:
|
||||
t = "upload blocked by xbu server config: %r"
|
||||
t = t % (vp,)
|
||||
self.log(t, 1)
|
||||
raise Pebkac(403, t)
|
||||
if hr.get("reloc"):
|
||||
|
@ -3424,10 +3444,9 @@ class Up2k(object):
|
|||
cur.connection.commit()
|
||||
|
||||
ap = djoin(job["ptop"], job["prel"], job["name"])
|
||||
times = (int(time.time()), int(cj["lmod"]))
|
||||
bos.utime(ap, times, False)
|
||||
mt = bos.utime_c(self.log, ap, int(cj["lmod"]), False, True)
|
||||
|
||||
self.log("touched %r from %d to %d" % (ap, job["lmod"], cj["lmod"]))
|
||||
self.log("touched %r from %d to %d" % (ap, job["lmod"], mt))
|
||||
except Exception as ex:
|
||||
self.log("umod failed, %r" % (ex,), 3)
|
||||
|
||||
|
@ -3461,7 +3480,7 @@ class Up2k(object):
|
|||
vrel = vjoin(job["prel"], fname)
|
||||
xlink = bool(vf.get("xlink"))
|
||||
cur, wark, _, _, _, _, _ = self._find_from_vpath(ptop, vrel)
|
||||
self._forget_file(ptop, vrel, cur, wark, True, st.st_size, xlink)
|
||||
self._forget_file(ptop, vrel, vf, cur, wark, True, st.st_size, xlink)
|
||||
except Exception as ex:
|
||||
self.log("skipping replace-relink: %r" % (ex,))
|
||||
finally:
|
||||
|
@ -3579,11 +3598,10 @@ class Up2k(object):
|
|||
t = "BUG: no valid sources to link from! orig(%r) fsrc(%r) link(%r)"
|
||||
self.log(t, 1)
|
||||
raise Exception(t % (src, fsrc, dst))
|
||||
shutil.copy2(fsenc(csrc), fsenc(dst))
|
||||
trystat_shutil_copy2(self.log, fsenc(csrc), fsenc(dst))
|
||||
|
||||
if lmod and (not linked or SYMTIME):
|
||||
times = (int(time.time()), int(lmod))
|
||||
bos.utime(dst, times, False)
|
||||
bos.utime_c(self.log, dst, int(lmod), False)
|
||||
|
||||
def handle_chunks(
|
||||
self, ptop: str, wark: str, chashes: list[str]
|
||||
|
@ -3666,14 +3684,15 @@ class Up2k(object):
|
|||
t = t.format(job["name"], nchunks[0][0], coffsets[0][0], cur_sz)
|
||||
raise Pebkac(400, t)
|
||||
|
||||
job["busy"][chash] = 1
|
||||
for chash in chashes:
|
||||
job["busy"][chash] = 1
|
||||
|
||||
job["poke"] = time.time()
|
||||
|
||||
return chashes, chunksize, coffsets, path, job["lmod"], job["size"], job["sprs"]
|
||||
|
||||
def fast_confirm_chunks(
|
||||
self, ptop: str, wark: str, chashes: list[str]
|
||||
self, ptop: str, wark: str, chashes: list[str], locked: list[str]
|
||||
) -> tuple[int, str]:
|
||||
if not self.mutex.acquire(False):
|
||||
return -1, ""
|
||||
|
@ -3681,7 +3700,7 @@ class Up2k(object):
|
|||
self.mutex.release()
|
||||
return -1, ""
|
||||
try:
|
||||
return self._confirm_chunks(ptop, wark, chashes, chashes)
|
||||
return self._confirm_chunks(ptop, wark, chashes, locked, False)
|
||||
finally:
|
||||
self.reg_mutex.release()
|
||||
self.mutex.release()
|
||||
|
@ -3690,10 +3709,10 @@ class Up2k(object):
|
|||
self, ptop: str, wark: str, written: list[str], locked: list[str]
|
||||
) -> tuple[int, str]:
|
||||
with self.mutex, self.reg_mutex:
|
||||
return self._confirm_chunks(ptop, wark, written, locked)
|
||||
return self._confirm_chunks(ptop, wark, written, locked, True)
|
||||
|
||||
def _confirm_chunks(
|
||||
self, ptop: str, wark: str, written: list[str], locked: list[str]
|
||||
self, ptop: str, wark: str, written: list[str], locked: list[str], final: bool
|
||||
) -> tuple[int, str]:
|
||||
if True:
|
||||
self.db_act = self.vol_act[ptop] = time.time()
|
||||
|
@ -3705,14 +3724,16 @@ class Up2k(object):
|
|||
except Exception as ex:
|
||||
return -2, "confirm_chunk, wark(%r)" % (ex,) # type: ignore
|
||||
|
||||
for chash in locked:
|
||||
for chash in locked if final else written:
|
||||
job["busy"].pop(chash, None)
|
||||
|
||||
try:
|
||||
for chash in written:
|
||||
job["need"].remove(chash)
|
||||
except Exception as ex:
|
||||
# dead tcp connections can get here by timeout (OK)
|
||||
for zs in locked:
|
||||
if job["busy"].pop(zs, None):
|
||||
self.log("panic-unlock wark(%s) chunk(%s)" % (wark, zs), 1)
|
||||
return -2, "confirm_chunk, chash(%s) %r" % (chash, ex) # type: ignore
|
||||
|
||||
ret = len(job["need"])
|
||||
|
@ -3756,10 +3777,8 @@ class Up2k(object):
|
|||
times = (int(time.time()), int(job["lmod"]))
|
||||
t = "no more chunks, setting times %s (%d) on %r"
|
||||
self.log(t % (times, bos.path.getsize(dst), dst))
|
||||
try:
|
||||
bos.utime(dst, times)
|
||||
except:
|
||||
self.log("failed to utime (%r, %s)" % (dst, times))
|
||||
bos.utime_c(self.log, dst, times[1], False)
|
||||
# the above logmsg (and associated logic) is retained due to unforget.py
|
||||
|
||||
zs = "prel name lmod size ptop vtop wark dwrk host user addr"
|
||||
z2 = [job[x] for x in zs.split()]
|
||||
|
@ -3878,7 +3897,9 @@ class Up2k(object):
|
|||
|
||||
return True
|
||||
|
||||
def db_rm(self, db: "sqlite3.Cursor", rd: str, fn: str, sz: int) -> None:
|
||||
def db_rm(
|
||||
self, db: "sqlite3.Cursor", vflags: dict[str, Any], rd: str, fn: str, sz: int
|
||||
) -> None:
|
||||
sql = "delete from up where rd = ? and fn = ?"
|
||||
try:
|
||||
r = db.execute(sql, (rd, fn))
|
||||
|
@ -3886,9 +3907,22 @@ class Up2k(object):
|
|||
assert self.mem_cur # !rm
|
||||
r = db.execute(sql, s3enc(self.mem_cur, rd, fn))
|
||||
|
||||
if r.rowcount:
|
||||
self.volsize[db] -= sz
|
||||
self.volnfiles[db] -= 1
|
||||
if not r.rowcount:
|
||||
return
|
||||
|
||||
self.volsize[db] -= sz
|
||||
self.volnfiles[db] -= 1
|
||||
|
||||
if "nodirsz" not in vflags:
|
||||
try:
|
||||
q = "update ds set nf=nf-1, sz=sz-? where rd=?"
|
||||
while True:
|
||||
db.execute(q, (sz, rd))
|
||||
if not rd:
|
||||
break
|
||||
rd = rd.rsplit("/", 1)[0] if "/" in rd else ""
|
||||
except:
|
||||
pass
|
||||
|
||||
def db_add(
|
||||
self,
|
||||
|
@ -3909,7 +3943,7 @@ class Up2k(object):
|
|||
skip_xau: bool = False,
|
||||
) -> None:
|
||||
"""mutex(main) me"""
|
||||
self.db_rm(db, rd, fn, sz)
|
||||
self.db_rm(db, vflags, rd, fn, sz)
|
||||
|
||||
if not ip:
|
||||
db_ip = ""
|
||||
|
@ -3950,8 +3984,11 @@ class Up2k(object):
|
|||
at or time.time(),
|
||||
"",
|
||||
)
|
||||
if not hr:
|
||||
t = "upload blocked by xau server config"
|
||||
t = hr.get("rejectmsg") or ""
|
||||
if t or not hr:
|
||||
if not t:
|
||||
t = "upload blocked by xau server config: %r"
|
||||
t = t % (djoin(vtop, rd, fn),)
|
||||
self.log(t, 1)
|
||||
wunlink(self.log, dst, vflags)
|
||||
self.registry[ptop].pop(wark, None)
|
||||
|
@ -4121,6 +4158,9 @@ class Up2k(object):
|
|||
except:
|
||||
raise Pebkac(400, "file not found on disk (already deleted?)")
|
||||
|
||||
if "bcasechk" in vn.flags and not vn.casechk(rem, False):
|
||||
raise Pebkac(400, "file does not exist case-sensitively")
|
||||
|
||||
scandir = not self.args.no_scandir
|
||||
if is_dir:
|
||||
# note: deletion inside shares would require a rewrite here;
|
||||
|
@ -4192,7 +4232,7 @@ class Up2k(object):
|
|||
xlink = bool(dbv.flags.get("xlink"))
|
||||
cur, wark, _, _, _, _, _ = self._find_from_vpath(ptop, volpath)
|
||||
self._forget_file(
|
||||
ptop, volpath, cur, wark, True, st.st_size, xlink
|
||||
ptop, volpath, dbv.flags, cur, wark, True, st.st_size, xlink
|
||||
)
|
||||
finally:
|
||||
if cur:
|
||||
|
@ -4245,6 +4285,9 @@ class Up2k(object):
|
|||
self.db_act = self.vol_act[svn_dbv.realpath] = time.time()
|
||||
|
||||
st = bos.stat(sabs)
|
||||
if "bcasechk" in svn.flags and not svn.casechk(srem, False):
|
||||
raise Pebkac(400, "file does not exist case-sensitively")
|
||||
|
||||
if stat.S_ISREG(st.st_mode) or stat.S_ISLNK(st.st_mode):
|
||||
with self.mutex:
|
||||
try:
|
||||
|
@ -4403,7 +4446,7 @@ class Up2k(object):
|
|||
b1, b2 = fsenc(sabs), fsenc(dabs)
|
||||
is_link = os.path.islink(b1) # due to _relink
|
||||
try:
|
||||
shutil.copy2(b1, b2)
|
||||
trystat_shutil_copy2(self.log, b1, b2)
|
||||
except:
|
||||
try:
|
||||
wunlink(self.log, dabs, dvn.flags)
|
||||
|
@ -4463,6 +4506,9 @@ class Up2k(object):
|
|||
raise Pebkac(400, "mv: cannot move a mountpoint")
|
||||
|
||||
st = bos.lstat(sabs)
|
||||
if "bcasechk" in svn.flags and not svn.casechk(srem, False):
|
||||
raise Pebkac(400, "file does not exist case-sensitively")
|
||||
|
||||
if stat.S_ISREG(st.st_mode) or stat.S_ISLNK(st.st_mode):
|
||||
with self.mutex:
|
||||
try:
|
||||
|
@ -4650,7 +4696,14 @@ class Up2k(object):
|
|||
|
||||
with self.reg_mutex:
|
||||
has_dupes = self._forget_file(
|
||||
svn.realpath, srem, c1, w, is_xvol, fsize_ or fsize, xlink
|
||||
svn.realpath,
|
||||
srem,
|
||||
svn.flags,
|
||||
c1,
|
||||
w,
|
||||
is_xvol,
|
||||
fsize_ or fsize,
|
||||
xlink,
|
||||
)
|
||||
|
||||
if not is_xvol:
|
||||
|
@ -4702,7 +4755,7 @@ class Up2k(object):
|
|||
b1, b2 = fsenc(sabs), fsenc(dabs)
|
||||
is_link = os.path.islink(b1) # due to _relink
|
||||
try:
|
||||
shutil.copy2(b1, b2)
|
||||
trystat_shutil_copy2(self.log, b1, b2)
|
||||
except:
|
||||
try:
|
||||
wunlink(self.log, dabs, dvn.flags)
|
||||
|
@ -4795,6 +4848,7 @@ class Up2k(object):
|
|||
self,
|
||||
ptop: str,
|
||||
vrem: str,
|
||||
vflags: dict[str, Any],
|
||||
cur: Optional["sqlite3.Cursor"],
|
||||
wark: Optional[str],
|
||||
drop_tags: bool,
|
||||
|
@ -4819,7 +4873,7 @@ class Up2k(object):
|
|||
q = "delete from mt where w=?"
|
||||
cur.execute(q, (wark[:16],))
|
||||
|
||||
self.db_rm(cur, srd, sfn, sz)
|
||||
self.db_rm(cur, vflags, srd, sfn, sz)
|
||||
|
||||
reg = self.registry.get(ptop)
|
||||
if reg:
|
||||
|
@ -4908,7 +4962,10 @@ class Up2k(object):
|
|||
mt = bos.path.getmtime(slabs, False)
|
||||
flags = self.flags.get(ptop) or {}
|
||||
atomic_move(self.log, sabs, slabs, flags)
|
||||
bos.utime(slabs, (int(time.time()), int(mt)), False)
|
||||
try:
|
||||
bos.utime(slabs, (int(time.time()), int(mt)), False)
|
||||
except:
|
||||
self.log("relink: failed to utime(%r, %s)" % (slabs, mt), 3)
|
||||
self._symlink(slabs, sabs, flags, False, is_mv=True)
|
||||
full[slabs] = (ptop, rem)
|
||||
sabs = slabs
|
||||
|
@ -4981,7 +5038,7 @@ class Up2k(object):
|
|||
for k in cj["hash"]:
|
||||
if not self.r_hash.match(k):
|
||||
raise Pebkac(
|
||||
400, "at least one hash is not according to spec: {}".format(k)
|
||||
400, "at least one hash is not according to spec: %r" % (k,)
|
||||
)
|
||||
|
||||
# try to use client-provided timestamp, don't care if it fails somehow
|
||||
|
@ -5038,6 +5095,16 @@ class Up2k(object):
|
|||
|
||||
return ret, st
|
||||
|
||||
def _ex_hash(self, ex: Exception, ap: str) -> None:
|
||||
eno = getattr(ex, "errno", 0)
|
||||
if eno in E_FS_MEH:
|
||||
return self.log("hashing failed; %r @ %r" % (ex, ap))
|
||||
if eno not in E_FS_CRIT:
|
||||
return self.log("hashing failed; %r @ %r\n%s" % (ex, ap, min_ex()), 3)
|
||||
t = "hashing failed; %r @ %r\n%s\nWARNING: This MAY indicate a serious issue with your harddisk or filesystem! Please investigate %sOS-logs\n"
|
||||
t2 = "" if ANYWIN or MACOS else "dmesg and "
|
||||
return self.log(t % (ex, ap, min_ex(), t2), 1)
|
||||
|
||||
def _new_upload(self, job: dict[str, Any], vfs: VFS, depth: int) -> dict[str, str]:
|
||||
pdir = djoin(job["ptop"], job["prel"])
|
||||
if not job["size"]:
|
||||
|
@ -5071,8 +5138,10 @@ class Up2k(object):
|
|||
job["t0"],
|
||||
"",
|
||||
)
|
||||
if not hr:
|
||||
t = "upload blocked by xbu server config: %r" % (vp_chk,)
|
||||
t = hr.get("rejectmsg") or ""
|
||||
if t or not hr:
|
||||
if not t:
|
||||
t = "upload blocked by xbu server config: %r" % (vp_chk,)
|
||||
self.log(t, 1)
|
||||
raise Pebkac(403, t)
|
||||
if hr.get("reloc"):
|
||||
|
@ -5134,7 +5203,7 @@ class Up2k(object):
|
|||
sprs = False
|
||||
|
||||
if not ANYWIN and sprs and sz > 1024 * 1024:
|
||||
fs = self.fstab.get(pdir)
|
||||
fs, mnt = self.fstab.get(pdir)
|
||||
if fs == "ok":
|
||||
pass
|
||||
elif "nosparse" in vf:
|
||||
|
@ -5220,17 +5289,21 @@ class Up2k(object):
|
|||
self.log("\n".join([t] + vis))
|
||||
for job in rm:
|
||||
del reg[job["wark"]]
|
||||
rsv_cleared = False
|
||||
try:
|
||||
# remove the filename reservation
|
||||
path = djoin(job["ptop"], job["prel"], job["name"])
|
||||
if bos.path.getsize(path) == 0:
|
||||
bos.unlink(path)
|
||||
rsv_cleared = True
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
if len(job["hash"]) == len(job["need"]):
|
||||
# PARTIAL is empty, delete that too
|
||||
if len(job["hash"]) == len(job["need"]) or (
|
||||
rsv_cleared and "rm_partial" in self.flags[job["ptop"]]
|
||||
):
|
||||
# PARTIAL is empty (hash==need) or --rm-partial, so delete that too
|
||||
path = djoin(job["ptop"], job["prel"], job["tnam"])
|
||||
bos.unlink(path)
|
||||
except:
|
||||
|
|
|
@ -52,9 +52,9 @@ from .__init__ import (
|
|||
VT100,
|
||||
WINDOWS,
|
||||
EnvParams,
|
||||
unicode,
|
||||
)
|
||||
from .__version__ import S_BUILD_DT, S_VERSION
|
||||
from .stolen import surrogateescape
|
||||
|
||||
try:
|
||||
from datetime import datetime, timezone
|
||||
|
@ -80,6 +80,9 @@ except:
|
|||
|
||||
if PY2:
|
||||
range = xrange # type: ignore
|
||||
from .stolen import surrogateescape
|
||||
|
||||
surrogateescape.register_surrogateescape()
|
||||
|
||||
|
||||
if sys.version_info >= (3, 7) or (
|
||||
|
@ -110,9 +113,17 @@ E_ADDR_NOT_AVAIL = _ens("EADDRNOTAVAIL WSAEADDRNOTAVAIL")
|
|||
E_ADDR_IN_USE = _ens("EADDRINUSE WSAEADDRINUSE")
|
||||
E_ACCESS = _ens("EACCES WSAEACCES")
|
||||
E_UNREACH = _ens("EHOSTUNREACH WSAEHOSTUNREACH ENETUNREACH WSAENETUNREACH")
|
||||
E_FS_MEH = _ens("EPERM EACCES ENOENT ENOTCAPABLE")
|
||||
E_FS_CRIT = _ens("EIO EFAULT EUCLEAN ENOTBLK")
|
||||
|
||||
IP6ALL = "0:0:0:0:0:0:0:0"
|
||||
IP6_LL = ("fe8", "fe9", "fea", "feb")
|
||||
IP64_LL = ("fe8", "fe9", "fea", "feb", "169.254")
|
||||
|
||||
UC_CDISP = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._"
|
||||
BC_CDISP = UC_CDISP.encode("ascii")
|
||||
UC_CDISP_SET = set(UC_CDISP)
|
||||
BC_CDISP_SET = set(BC_CDISP)
|
||||
|
||||
try:
|
||||
import fcntl
|
||||
|
@ -127,6 +138,25 @@ try:
|
|||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
if os.environ.get("PRTY_NO_IFADDR"):
|
||||
raise Exception()
|
||||
try:
|
||||
if os.getenv("PRTY_SYS_ALL") or os.getenv("PRTY_SYS_IFADDR"):
|
||||
raise ImportError()
|
||||
|
||||
from .stolen.ifaddr import get_adapters
|
||||
except ImportError:
|
||||
from ifaddr import get_adapters
|
||||
|
||||
HAVE_IFADDR = True
|
||||
except:
|
||||
HAVE_IFADDR = False
|
||||
|
||||
def get_adapters(include_unconfigured=False):
|
||||
return []
|
||||
|
||||
|
||||
try:
|
||||
if os.environ.get("PRTY_NO_SQLITE"):
|
||||
raise Exception()
|
||||
|
@ -164,6 +194,11 @@ try:
|
|||
except:
|
||||
pass
|
||||
|
||||
if os.getenv("PRTY_MODSPEC"):
|
||||
from inspect import getsourcefile
|
||||
|
||||
print("PRTY_MODSPEC: ifaddr:", getsourcefile(get_adapters))
|
||||
|
||||
if True: # pylint: disable=using-constant-test
|
||||
import types
|
||||
from collections.abc import Callable, Iterable
|
||||
|
@ -260,7 +295,6 @@ RE_MEMAVAIL = re.compile("^MemAvailable:.* kB")
|
|||
BOS_SEP = ("%s" % (os.sep,)).encode("ascii")
|
||||
|
||||
|
||||
surrogateescape.register_surrogateescape()
|
||||
if WINDOWS and PY2:
|
||||
FS_ENCODING = "utf-8"
|
||||
else:
|
||||
|
@ -369,6 +403,13 @@ DAV_ALLPROP_L = [
|
|||
DAV_ALLPROPS = set(DAV_ALLPROP_L)
|
||||
|
||||
|
||||
FAVICON_MIMES = {
|
||||
"gif": "image/gif",
|
||||
"png": "image/png",
|
||||
"svg": "image/svg+xml",
|
||||
}
|
||||
|
||||
|
||||
MIMES = {
|
||||
"opus": "audio/ogg; codecs=opus",
|
||||
"owa": "audio/webm; codecs=opus",
|
||||
|
@ -423,7 +464,7 @@ EXTS["vnd.mozilla.apng"] = "png"
|
|||
MAGIC_MAP = {"jpeg": "jpg"}
|
||||
|
||||
|
||||
DEF_EXP = "self.ip self.ua self.uname self.host cfg.name cfg.logout vf.scan vf.thsize hdr.cf_ipcountry srv.itime srv.htime"
|
||||
DEF_EXP = "self.ip self.ua self.uname self.host cfg.name cfg.logout vf.scan vf.thsize hdr.cf-ipcountry srv.itime srv.htime"
|
||||
|
||||
DEF_MTE = ".files,circle,album,.tn,artist,title,.bpm,key,.dur,.q,.vq,.aq,vc,ac,fmt,res,.fps,ahash,vhash"
|
||||
|
||||
|
@ -539,6 +580,8 @@ def py_desc() -> str:
|
|||
ofs = py_ver.find(".final.")
|
||||
if ofs > 0:
|
||||
py_ver = py_ver[:ofs]
|
||||
if "free-threading" in sys.version:
|
||||
py_ver += "t"
|
||||
|
||||
host_os = platform.system()
|
||||
compiler = platform.python_compiler().split("http")[0]
|
||||
|
@ -649,6 +692,9 @@ def read_utf8(log: Optional["NamedLogger"], ap: Union[str, bytes], strict: bool)
|
|||
with open(ap, "rb") as f:
|
||||
buf = f.read()
|
||||
|
||||
if buf.startswith(b"\xef\xbb\xbf"):
|
||||
buf = buf[3:]
|
||||
|
||||
try:
|
||||
return buf.decode("utf-8", "strict")
|
||||
except UnicodeDecodeError as ex:
|
||||
|
@ -1114,16 +1160,18 @@ class ProgressPrinter(threading.Thread):
|
|||
sigblock()
|
||||
tp = 0
|
||||
msg = None
|
||||
no_stdout = self.args.q
|
||||
slp_pr = self.args.scan_pr_r
|
||||
slp_ps = min(slp_pr, self.args.scan_st_r)
|
||||
no_stdout = self.args.q or slp_pr == slp_ps
|
||||
fmt = " {}\033[K\r" if VT100 else " {} $\r"
|
||||
while not self.end:
|
||||
time.sleep(0.1)
|
||||
time.sleep(slp_ps)
|
||||
if msg == self.msg or self.end:
|
||||
continue
|
||||
|
||||
msg = self.msg
|
||||
now = time.time()
|
||||
if msg and now - tp > 10:
|
||||
if msg and now - tp >= slp_pr:
|
||||
tp = now
|
||||
self.log("progress: %r" % (msg,), 6)
|
||||
|
||||
|
@ -1181,21 +1229,21 @@ class MTHash(object):
|
|||
for nch in range(nchunks):
|
||||
self.work_q.put(nch)
|
||||
|
||||
ex = ""
|
||||
ex: Optional[Exception] = None
|
||||
for nch in range(nchunks):
|
||||
qe = self.done_q.get()
|
||||
try:
|
||||
nch, dig, ofs, csz = qe
|
||||
chunks[nch] = (dig, ofs, csz)
|
||||
except:
|
||||
ex = ex or str(qe)
|
||||
ex = ex or qe # type: ignore
|
||||
|
||||
if pp:
|
||||
mb = (fsz - nch * chunksz) // (1024 * 1024)
|
||||
pp.msg = prefix + str(mb) + suffix
|
||||
|
||||
if ex:
|
||||
raise Exception(ex)
|
||||
raise ex
|
||||
|
||||
ret = []
|
||||
for n in range(nchunks):
|
||||
|
@ -1212,7 +1260,7 @@ class MTHash(object):
|
|||
try:
|
||||
v = self.hash_at(ofs)
|
||||
except Exception as ex:
|
||||
v = str(ex) # type: ignore
|
||||
v = ex # type: ignore
|
||||
|
||||
self.done_q.put(v)
|
||||
|
||||
|
@ -1570,10 +1618,12 @@ def vol_san(vols: list["VFS"], txt: bytes) -> bytes:
|
|||
bvp = vol.vpath.encode("utf-8")
|
||||
bvph = b"$hist(/" + bvp + b")"
|
||||
|
||||
txt = txt.replace(bap, bvp)
|
||||
txt = txt.replace(bhp, bvph)
|
||||
txt = txt.replace(bap.replace(b"\\", b"\\\\"), bvp)
|
||||
txt = txt.replace(bhp.replace(b"\\", b"\\\\"), bvph)
|
||||
if bap:
|
||||
txt = txt.replace(bap, bvp)
|
||||
txt = txt.replace(bap.replace(b"\\", b"\\\\"), bvp)
|
||||
if bhp:
|
||||
txt = txt.replace(bhp, bvph)
|
||||
txt = txt.replace(bhp.replace(b"\\", b"\\\\"), bvph)
|
||||
|
||||
if vol.histpath != vol.dbpath:
|
||||
bdp = vol.dbpath.encode("utf-8")
|
||||
|
@ -1752,12 +1802,12 @@ class MultipartParser(object):
|
|||
continue
|
||||
|
||||
if m.group(1).lower() != "form-data":
|
||||
raise Pebkac(400, "not form-data: {}".format(ln))
|
||||
raise Pebkac(400, "not form-data: %r" % (ln,))
|
||||
|
||||
try:
|
||||
field = self.re_cdisp_field.match(ln).group(1) # type: ignore
|
||||
except:
|
||||
raise Pebkac(400, "missing field name: {}".format(ln))
|
||||
raise Pebkac(400, "missing field name: %r" % (ln,))
|
||||
|
||||
try:
|
||||
fn = self.re_cdisp_file.match(ln).group(1) # type: ignore
|
||||
|
@ -1929,7 +1979,7 @@ def get_boundary(headers: dict[str, str]) -> str:
|
|||
ct = headers["content-type"]
|
||||
m = re.match(ptn, ct, re.IGNORECASE)
|
||||
if not m:
|
||||
raise Pebkac(400, "invalid content-type for a multipart post: {}".format(ct))
|
||||
raise Pebkac(400, "invalid content-type for a multipart post: %r" % (ct,))
|
||||
|
||||
return m.group(2)
|
||||
|
||||
|
@ -2071,6 +2121,29 @@ def gencookie(
|
|||
)
|
||||
|
||||
|
||||
def gen_content_disposition(fn: str) -> str:
|
||||
safe = UC_CDISP_SET
|
||||
bsafe = BC_CDISP_SET
|
||||
fn = fn.replace("/", "_").replace("\\", "_")
|
||||
zb = fn.encode("utf-8", "xmlcharrefreplace")
|
||||
if not PY2:
|
||||
zbl = [
|
||||
chr(x).encode("utf-8")
|
||||
if x in bsafe
|
||||
else "%{:02X}".format(x).encode("ascii")
|
||||
for x in zb
|
||||
]
|
||||
else:
|
||||
zbl = [unicode(x) if x in bsafe else "%{:02X}".format(ord(x)) for x in zb]
|
||||
|
||||
ufn = b"".join(zbl).decode("ascii")
|
||||
afn = "".join([x if x in safe else "_" for x in fn]).lstrip(".")
|
||||
while ".." in afn:
|
||||
afn = afn.replace("..", ".")
|
||||
|
||||
return "attachment; filename=\"%s\"; filename*=UTF-8''%s" % (afn, ufn)
|
||||
|
||||
|
||||
def humansize(sz: float, terse: bool = False) -> str:
|
||||
for unit in HUMANSIZE_UNITS:
|
||||
if sz < 1024:
|
||||
|
@ -2078,6 +2151,7 @@ def humansize(sz: float, terse: bool = False) -> str:
|
|||
|
||||
sz /= 1024.0
|
||||
|
||||
assert unit # type: ignore # !rm
|
||||
if terse:
|
||||
return "%s%s" % (str(sz)[:4].rstrip("."), unit[:1])
|
||||
else:
|
||||
|
@ -2230,14 +2304,14 @@ def odfusion(
|
|||
ret = base.copy()
|
||||
if oth.startswith("+"):
|
||||
for k in words1:
|
||||
ret[k] = True
|
||||
ret[k] = True # type: ignore
|
||||
elif oth[:1] in ("-", "/"):
|
||||
for k in words1:
|
||||
ret.pop(k, None)
|
||||
ret.pop(k, None) # type: ignore
|
||||
else:
|
||||
ret = ODict.fromkeys(words0, True)
|
||||
|
||||
return ret
|
||||
return ret # type: ignore
|
||||
|
||||
|
||||
def ipnorm(ip: str) -> str:
|
||||
|
@ -2609,6 +2683,24 @@ def set_fperms(f: Union[typing.BinaryIO, typing.IO[Any]], vf: dict[str, Any]) ->
|
|||
os.fchown(fno, vf["uid"], vf["gid"])
|
||||
|
||||
|
||||
def trystat_shutil_copy2(log: "NamedLogger", src: bytes, dst: bytes) -> bytes:
|
||||
try:
|
||||
return shutil.copy2(src, dst)
|
||||
except:
|
||||
# ignore failed mtime on linux+ntfs; for example:
|
||||
# shutil.py:437 <copy2>: copystat(src, dst, follow_symlinks=follow_symlinks)
|
||||
# shutil.py:376 <copystat>: lookup("utime")(dst, ns=(st.st_atime_ns, st.st_mtime_ns),
|
||||
# [PermissionError] [Errno 1] Operation not permitted, '/windows/_videos'
|
||||
_, _, tb = sys.exc_info()
|
||||
for _, _, fun, _ in traceback.extract_tb(tb):
|
||||
if fun == "copystat":
|
||||
if log:
|
||||
t = "warning: failed to retain some file attributes (timestamp and/or permissions) during copy from %r to %r:\n%s"
|
||||
log(t % (src, dst, min_ex()), 3)
|
||||
return dst # close enough
|
||||
raise
|
||||
|
||||
|
||||
def _fs_mvrm(
|
||||
log: "NamedLogger", src: str, dst: str, atomic: bool, flags: dict[str, Any]
|
||||
) -> bool:
|
||||
|
@ -2647,7 +2739,7 @@ def _fs_mvrm(
|
|||
t = "something appeared at dst; aborting rename %r ==> %r"
|
||||
log(t % (src, dst), 1)
|
||||
return False
|
||||
osfun(*args)
|
||||
osfun(*args) # type: ignore
|
||||
if attempt:
|
||||
now = time.time()
|
||||
t = "%sd in %.2f sec, attempt %d: %r"
|
||||
|
@ -2697,7 +2789,7 @@ def atomic_move(log: "NamedLogger", src: str, dst: str, flags: dict[str, Any]) -
|
|||
os.unlink(bdst)
|
||||
except:
|
||||
pass
|
||||
shutil.move(bsrc, bdst)
|
||||
shutil.move(bsrc, bdst) # type: ignore
|
||||
|
||||
|
||||
def wunlink(log: "NamedLogger", abspath: str, flags: dict[str, Any]) -> bool:
|
||||
|
@ -2740,6 +2832,8 @@ def get_df(abspath: str, prune: bool) -> tuple[int, int, str]:
|
|||
if not ANYWIN and not MACOS:
|
||||
|
||||
def siocoutq(sck: socket.socket) -> int:
|
||||
assert fcntl # type: ignore # !rm
|
||||
assert termios # type: ignore # !rm
|
||||
# SIOCOUTQ^sockios.h == TIOCOUTQ^ioctl.h
|
||||
try:
|
||||
zb = fcntl.ioctl(sck.fileno(), termios.TIOCOUTQ, b"AAAA")
|
||||
|
@ -2859,8 +2953,6 @@ def read_socket_chunked(
|
|||
|
||||
|
||||
def list_ips() -> list[str]:
|
||||
from .stolen.ifaddr import get_adapters
|
||||
|
||||
ret: set[str] = set()
|
||||
for nic in get_adapters():
|
||||
for ipo in nic.ips:
|
||||
|
@ -3103,7 +3195,7 @@ def sendfile_kern(
|
|||
try:
|
||||
req = min(0x2000000, upper - ofs) # 32 MiB
|
||||
if use_poll:
|
||||
poll.poll(10000)
|
||||
poll.poll(10000) # type: ignore
|
||||
else:
|
||||
select.select([], [out_fd], [], 10)
|
||||
n = os.sendfile(out_fd, in_fd, ofs, req)
|
||||
|
@ -3159,8 +3251,9 @@ def statdir(
|
|||
else:
|
||||
src = "listdir"
|
||||
fun: Any = os.lstat if lstat else os.stat
|
||||
btop_ = os.path.join(btop, b"")
|
||||
for name in os.listdir(btop):
|
||||
abspath = os.path.join(btop, name)
|
||||
abspath = btop_ + name
|
||||
try:
|
||||
yield (fsdec(name), fun(abspath))
|
||||
except Exception as ex:
|
||||
|
@ -3199,7 +3292,9 @@ def rmdirs(
|
|||
|
||||
stats = statdir(logger, scandir, lstat, top, False)
|
||||
dirs = [x[0] for x in stats if stat.S_ISDIR(x[1].st_mode)]
|
||||
dirs = [os.path.join(top, x) for x in dirs]
|
||||
if dirs:
|
||||
top_ = os.path.join(top, "")
|
||||
dirs = [top_ + x for x in dirs]
|
||||
ok = []
|
||||
ng = []
|
||||
for d in reversed(dirs):
|
||||
|
@ -3390,7 +3485,9 @@ NICEB = NICES.encode("utf-8")
|
|||
|
||||
|
||||
def runcmd(
|
||||
argv: Union[list[bytes], list[str]], timeout: Optional[float] = None, **ka: Any
|
||||
argv: Union[list[bytes], list[str], list["LiteralString"]],
|
||||
timeout: Optional[float] = None,
|
||||
**ka: Any
|
||||
) -> tuple[int, str, str]:
|
||||
isbytes = isinstance(argv[0], (bytes, bytearray))
|
||||
oom = ka.pop("oom", 0) # 0..1000
|
||||
|
@ -3409,19 +3506,19 @@ def runcmd(
|
|||
if ANYWIN:
|
||||
if isbytes:
|
||||
if argv[0] in CMD_EXEB:
|
||||
argv[0] += b".exe"
|
||||
argv[0] += b".exe" # type: ignore
|
||||
else:
|
||||
if argv[0] in CMD_EXES:
|
||||
argv[0] += ".exe"
|
||||
argv[0] += ".exe" # type: ignore
|
||||
|
||||
if ka.pop("nice", None):
|
||||
if WINDOWS:
|
||||
ka["creationflags"] = 0x4000
|
||||
elif NICEB:
|
||||
if isbytes:
|
||||
argv = [NICEB] + argv
|
||||
argv = [NICEB] + argv # type: ignore
|
||||
else:
|
||||
argv = [NICES] + argv
|
||||
argv = [NICES] + argv # type: ignore
|
||||
|
||||
p = sp.Popen(argv, stdout=cout, stderr=cerr, **ka)
|
||||
|
||||
|
@ -3433,10 +3530,10 @@ def runcmd(
|
|||
pass
|
||||
|
||||
if not timeout or PY2:
|
||||
bout, berr = p.communicate(sin)
|
||||
bout, berr = p.communicate(sin) # type: ignore
|
||||
else:
|
||||
try:
|
||||
bout, berr = p.communicate(sin, timeout=timeout)
|
||||
bout, berr = p.communicate(sin, timeout=timeout) # type: ignore
|
||||
except sp.TimeoutExpired:
|
||||
if kill == "n":
|
||||
return -18, "", "" # SIGCONT; leave it be
|
||||
|
@ -3446,7 +3543,7 @@ def runcmd(
|
|||
killtree(p.pid)
|
||||
|
||||
try:
|
||||
bout, berr = p.communicate(timeout=1)
|
||||
bout, berr = p.communicate(timeout=1) # type: ignore
|
||||
except:
|
||||
bout = b""
|
||||
berr = b""
|
||||
|
@ -3532,11 +3629,13 @@ def retchk(
|
|||
|
||||
def _parsehook(
|
||||
log: Optional["NamedLogger"], cmd: str
|
||||
) -> tuple[str, bool, bool, bool, float, dict[str, Any], list[str]]:
|
||||
) -> tuple[str, bool, bool, bool, bool, bool, float, dict[str, Any], list[str]]:
|
||||
areq = ""
|
||||
chk = False
|
||||
fork = False
|
||||
jtxt = False
|
||||
imp = False
|
||||
sin = False
|
||||
wait = 0.0
|
||||
tout = 0.0
|
||||
kill = "t"
|
||||
|
@ -3550,6 +3649,10 @@ def _parsehook(
|
|||
fork = True
|
||||
elif arg == "j":
|
||||
jtxt = True
|
||||
elif arg == "I":
|
||||
imp = True
|
||||
elif arg == "s":
|
||||
sin = True
|
||||
elif arg.startswith("w"):
|
||||
wait = float(arg[1:])
|
||||
elif arg.startswith("t"):
|
||||
|
@ -3594,7 +3697,7 @@ def _parsehook(
|
|||
|
||||
argv[0] = os.path.expandvars(os.path.expanduser(argv[0]))
|
||||
|
||||
return areq, chk, fork, jtxt, wait, sp_ka, argv
|
||||
return areq, chk, imp, fork, sin, jtxt, wait, sp_ka, argv
|
||||
|
||||
|
||||
def runihook(
|
||||
|
@ -3604,7 +3707,7 @@ def runihook(
|
|||
vol: "VFS",
|
||||
ups: list[tuple[str, int, int, str, str, str, int, str]],
|
||||
) -> bool:
|
||||
_, chk, fork, jtxt, wait, sp_ka, acmd = _parsehook(log, cmd)
|
||||
_, chk, _, fork, _, jtxt, wait, sp_ka, acmd = _parsehook(log, cmd)
|
||||
bcmd = [sfsenc(x) for x in acmd]
|
||||
if acmd[0].endswith(".py"):
|
||||
bcmd = [sfsenc(pybin)] + bcmd
|
||||
|
@ -3783,7 +3886,7 @@ def _runhook(
|
|||
txt: str,
|
||||
) -> dict[str, Any]:
|
||||
ret = {"rc": 0}
|
||||
areq, chk, fork, jtxt, wait, sp_ka, acmd = _parsehook(log, cmd)
|
||||
areq, chk, imp, fork, sin, jtxt, wait, sp_ka, acmd = _parsehook(log, cmd)
|
||||
if areq:
|
||||
for ch in areq:
|
||||
if ch not in perms:
|
||||
|
@ -3791,7 +3894,7 @@ def _runhook(
|
|||
if log:
|
||||
log(t % (uname, cmd, areq, perms))
|
||||
return ret # fallthrough to next hook
|
||||
if jtxt:
|
||||
if imp or jtxt:
|
||||
ja = {
|
||||
"ap": ap,
|
||||
"vp": vp,
|
||||
|
@ -3805,6 +3908,10 @@ def _runhook(
|
|||
"src": src,
|
||||
"txt": txt,
|
||||
}
|
||||
if imp:
|
||||
ja["log"] = log
|
||||
mod = loadpy(acmd[0], False)
|
||||
return mod.main(ja)
|
||||
arg = json.dumps(ja)
|
||||
else:
|
||||
arg = txt or ap
|
||||
|
@ -3815,7 +3922,11 @@ def _runhook(
|
|||
raise Exception("zmq says %d" % (zi,))
|
||||
return {"rc": 0, "stdout": zs}
|
||||
|
||||
acmd += [arg]
|
||||
if sin:
|
||||
sp_ka["sin"] = (arg + "\n").encode("utf-8", "replace")
|
||||
else:
|
||||
acmd += [arg]
|
||||
|
||||
if acmd[0].endswith(".py"):
|
||||
acmd = [pybin] + acmd
|
||||
|
||||
|
@ -3869,7 +3980,7 @@ def runhook(
|
|||
txt: str,
|
||||
) -> dict[str, Any]:
|
||||
assert broker or up2k # !rm
|
||||
args = (broker or up2k).args
|
||||
args = (broker or up2k).args # type: ignore
|
||||
verbose = args.hook_v
|
||||
vp = vp.replace("\\", "/")
|
||||
ret = {"rc": 0}
|
||||
|
@ -3887,6 +3998,7 @@ def runhook(
|
|||
if broker:
|
||||
broker.say("up2k.hook_fx", k, v, vp)
|
||||
else:
|
||||
assert up2k # !rm
|
||||
up2k.fx_backlog.append((k, v, vp))
|
||||
elif k == "reloc" and v:
|
||||
# idk, just take the last one ig
|
||||
|
@ -3899,7 +4011,7 @@ def runhook(
|
|||
else:
|
||||
ret[k] = v
|
||||
except Exception as ex:
|
||||
(log or print)("hook: {}".format(ex))
|
||||
(log or print)("hook: %r, %s" % (ex, ex))
|
||||
if ",c," in "," + cmd:
|
||||
return {}
|
||||
break
|
||||
|
@ -4046,6 +4158,8 @@ def termsize() -> tuple[int, int]:
|
|||
env = os.environ
|
||||
|
||||
def ioctl_GWINSZ(fd: int) -> Optional[tuple[int, int]]:
|
||||
assert fcntl # type: ignore # !rm
|
||||
assert termios # type: ignore # !rm
|
||||
try:
|
||||
cr = sunpack(b"hh", fcntl.ioctl(fd, termios.TIOCGWINSZ, b"AAAA"))
|
||||
return cr[::-1]
|
||||
|
@ -4175,7 +4289,7 @@ def _pkg_resource_exists(pkg: str, name: str) -> bool:
|
|||
|
||||
|
||||
def stat_resource(E: EnvParams, name: str):
|
||||
path = os.path.join(E.mod, name)
|
||||
path = E.mod_ + name
|
||||
if os.path.exists(path):
|
||||
return os.stat(fsenc(path))
|
||||
return None
|
||||
|
@ -4222,7 +4336,7 @@ def _has_resource(name: str):
|
|||
|
||||
|
||||
def has_resource(E: EnvParams, name: str):
|
||||
return _has_resource(name) or os.path.exists(os.path.join(E.mod, name))
|
||||
return _has_resource(name) or os.path.exists(E.mod_ + name)
|
||||
|
||||
|
||||
def load_resource(E: EnvParams, name: str, mode="rb") -> IO[bytes]:
|
||||
|
@ -4247,7 +4361,7 @@ def load_resource(E: EnvParams, name: str, mode="rb") -> IO[bytes]:
|
|||
stream = codecs.getreader(enc)(stream)
|
||||
return stream
|
||||
|
||||
ap = os.path.join(E.mod, name)
|
||||
ap = E.mod_ + name
|
||||
|
||||
if PY2:
|
||||
return codecs.open(ap, "r", encoding=enc) # type: ignore
|
||||
|
|
|
@ -22,8 +22,9 @@ window.baguetteBox = (function () {
|
|||
afterHide: null,
|
||||
duringHide: null,
|
||||
onChange: null,
|
||||
readDirRtl: false,
|
||||
},
|
||||
overlay, slider, btnPrev, btnNext, btnHelp, btnAnim, btnRotL, btnRotR, btnSel, btnFull, btnVmode, btnClose,
|
||||
overlay, slider, btnPrev, btnNext, btnHelp, btnAnim, btnRotL, btnRotR, btnSel, btnFull, btnVmode, btnReadDir, btnClose,
|
||||
currentGallery = [],
|
||||
currentIndex = 0,
|
||||
isOverlayVisible = false,
|
||||
|
@ -44,6 +45,7 @@ window.baguetteBox = (function () {
|
|||
loopA = null,
|
||||
loopB = null,
|
||||
url_ts = null,
|
||||
un_pp = 0,
|
||||
resume_mp = false;
|
||||
|
||||
var onFSC = function (e) {
|
||||
|
@ -72,10 +74,10 @@ window.baguetteBox = (function () {
|
|||
var touchEvent = e.touches[0] || e.changedTouches[0];
|
||||
if (touchEvent.pageX - touch.startX > 40) {
|
||||
touchFlag = true;
|
||||
showPreviousImage();
|
||||
showLeftImage();
|
||||
} else if (touchEvent.pageX - touch.startX < -40) {
|
||||
touchFlag = true;
|
||||
showNextImage();
|
||||
showRightImage();
|
||||
} else if (touch.startY - touchEvent.pageY > 100) {
|
||||
hideOverlay();
|
||||
}
|
||||
|
@ -113,9 +115,9 @@ window.baguetteBox = (function () {
|
|||
scrollTimer = Date.now();
|
||||
|
||||
if (d > 0)
|
||||
showNextImage();
|
||||
showNextImageIgnoreReadDir();
|
||||
else
|
||||
showPreviousImage();
|
||||
showPreviousImageIgnoreReadDir();
|
||||
};
|
||||
|
||||
var trapFocusInsideOverlay = function (e) {
|
||||
|
@ -211,6 +213,7 @@ window.baguetteBox = (function () {
|
|||
'<div id="bbox-btns">' +
|
||||
'<button id="bbox-help" type="button">?</button>' +
|
||||
'<button id="bbox-anim" type="button" tt="a">-</button>' +
|
||||
'<button id="bbox-readdir" type="button" tt="a">ltr</button>' +
|
||||
'<button id="bbox-rotl" type="button">↶</button>' +
|
||||
'<button id="bbox-rotr" type="button">↷</button>' +
|
||||
'<button id="bbox-tsel" type="button">sel</button>' +
|
||||
|
@ -228,6 +231,7 @@ window.baguetteBox = (function () {
|
|||
btnNext = ebi('bbox-next');
|
||||
btnHelp = ebi('bbox-help');
|
||||
btnAnim = ebi('bbox-anim');
|
||||
btnReadDir = ebi('bbox-readdir');
|
||||
btnRotL = ebi('bbox-rotl');
|
||||
btnRotR = ebi('bbox-rotr');
|
||||
btnSel = ebi('bbox-tsel');
|
||||
|
@ -252,7 +256,7 @@ window.baguetteBox = (function () {
|
|||
['S', 'toggle file selection'],
|
||||
['space, P, K', 'video: play / pause'],
|
||||
['U', 'video: seek 10sec back'],
|
||||
['P', 'video: seek 10sec ahead'],
|
||||
['O', 'video: seek 10sec ahead'],
|
||||
['0..9', 'video: seek 0%..90%'],
|
||||
['M', 'video: toggle mute'],
|
||||
['V', 'video: toggle loop'],
|
||||
|
@ -278,58 +282,68 @@ window.baguetteBox = (function () {
|
|||
if (modal.busy)
|
||||
return;
|
||||
|
||||
if (e.key == '?')
|
||||
return halp();
|
||||
|
||||
if (anymod(e, true))
|
||||
return;
|
||||
|
||||
var k = (e.code || e.key) + '', v = vid(), pos = -1;
|
||||
var k = (e.key || e.code) + '', v = vid();
|
||||
|
||||
if (k == "BracketLeft")
|
||||
if (k.startsWith('Key'))
|
||||
k = k.slice(3);
|
||||
else if (k.startsWith('Digit'))
|
||||
k = k.slice(5);
|
||||
|
||||
var kl = k.toLowerCase();
|
||||
|
||||
if (k == '?')
|
||||
return halp();
|
||||
|
||||
if (k == "[" || k == "BracketLeft")
|
||||
setloop(1);
|
||||
else if (k == "BracketRight")
|
||||
else if (k == "]" || k == "BracketRight")
|
||||
setloop(2);
|
||||
else if (e.shiftKey && k != "KeyR" && k != "R")
|
||||
else if (e.shiftKey && kl != "r")
|
||||
return;
|
||||
else if (k == "ArrowLeft" || k == "KeyJ" || k == "Left" || k == "j")
|
||||
showPreviousImage();
|
||||
else if (k == "ArrowRight" || k == "KeyL" || k == "Right" || k == "l")
|
||||
showNextImage();
|
||||
else if (k == "ArrowLeft" || k == "Left" || kl == "j")
|
||||
showLeftImage();
|
||||
else if (k == "ArrowRight" || k == "Right" || kl == "l")
|
||||
showRightImage();
|
||||
else if (k == "Escape" || k == "Esc")
|
||||
hideOverlay();
|
||||
else if (k == "Home")
|
||||
showFirstImage(e);
|
||||
else if (k == "End")
|
||||
showLastImage(e);
|
||||
else if (k == "Space" || k == "KeyP" || k == "KeyK")
|
||||
else if (k == "Space" || k == "Spacebar" || kl == " " || kl == "p" || kl == "k")
|
||||
playpause();
|
||||
else if (k == "KeyU" || k == "KeyO")
|
||||
relseek(k == "KeyU" ? -10 : 10);
|
||||
else if (k.indexOf('Digit') === 0 && v)
|
||||
v.currentTime = v.duration * parseInt(k.slice(-1)) * 0.1;
|
||||
else if (k == "KeyM" && v) {
|
||||
else if (kl == "u" || kl == "o")
|
||||
relseek(kl == "u" ? -10 : 10);
|
||||
else if (v && /^[0-9]$/.test(k))
|
||||
v.currentTime = v.duration * parseInt(k) * 0.1;
|
||||
else if (kl == "m" && v) {
|
||||
v.muted = vmute = !vmute;
|
||||
mp_ctl();
|
||||
}
|
||||
else if (k == "KeyV" && v) {
|
||||
else if (kl == "v" && v) {
|
||||
vloop = !vloop;
|
||||
vnext = vnext && !vloop;
|
||||
setVmode();
|
||||
}
|
||||
else if (k == "KeyC" && v) {
|
||||
else if (kl == "c" && v) {
|
||||
vnext = !vnext;
|
||||
vloop = vloop && !vnext;
|
||||
setVmode();
|
||||
}
|
||||
else if (k == "KeyF")
|
||||
else if (kl == "f")
|
||||
tglfull();
|
||||
else if (k == "KeyS" || k == "s")
|
||||
else if (kl == "s")
|
||||
tglsel();
|
||||
else if (k == "KeyR" || k == "r" || k == "R")
|
||||
else if (kl == "r")
|
||||
rotn(e.shiftKey ? -1 : 1);
|
||||
else if (k == "KeyY")
|
||||
else if (kl == "y")
|
||||
dlpic();
|
||||
else
|
||||
return;
|
||||
return ev(e);
|
||||
}
|
||||
|
||||
function anim() {
|
||||
|
@ -342,6 +356,21 @@ window.baguetteBox = (function () {
|
|||
tt.show.call(this);
|
||||
}
|
||||
|
||||
function toggleReadDir() {
|
||||
var o = options,
|
||||
next = options.readDirRtl ? "ltr" : "rtl";
|
||||
swrite('greaddir', next);
|
||||
slider.className = "no-transition";
|
||||
options = {};
|
||||
setOptions(o);
|
||||
updateOffset(true);
|
||||
window.getComputedStyle(slider).opacity; // force a restyle
|
||||
slider.className = "";
|
||||
|
||||
if (tt.en)
|
||||
tt.show.call(this);
|
||||
}
|
||||
|
||||
function setVmode() {
|
||||
var v = vid();
|
||||
ebi('bbox-vmode').style.display = v ? '' : 'none';
|
||||
|
@ -450,10 +479,12 @@ window.baguetteBox = (function () {
|
|||
if (anymod(e))
|
||||
return;
|
||||
|
||||
var k = e.code + '';
|
||||
var k = (e.key || e.code) + '';
|
||||
|
||||
if (k == "Space")
|
||||
ev(e);
|
||||
if (k == "Space" || k == "Spacebar" || k == " ") {
|
||||
un_pp = Date.now();
|
||||
return ev(e);
|
||||
}
|
||||
}
|
||||
|
||||
var passiveSupp = false;
|
||||
|
@ -479,12 +510,13 @@ window.baguetteBox = (function () {
|
|||
bind(document, 'fullscreenchange', onFSC);
|
||||
bind(overlay, 'click', overlayClickHandler);
|
||||
bind(overlay, 'wheel', overlayWheelHandler);
|
||||
bind(btnPrev, 'click', showPreviousImage);
|
||||
bind(btnNext, 'click', showNextImage);
|
||||
bind(btnPrev, 'click', showLeftImage);
|
||||
bind(btnNext, 'click', showRightImage);
|
||||
bind(btnClose, 'click', hideOverlay);
|
||||
bind(btnVmode, 'click', tglVmode);
|
||||
bind(btnHelp, 'click', halp);
|
||||
bind(btnAnim, 'click', anim);
|
||||
bind(btnReadDir, 'click', toggleReadDir);
|
||||
bind(btnRotL, 'click', rotl);
|
||||
bind(btnRotR, 'click', rotr);
|
||||
bind(btnSel, 'click', tglsel);
|
||||
|
@ -502,12 +534,13 @@ window.baguetteBox = (function () {
|
|||
unbind(document, 'fullscreenchange', onFSC);
|
||||
unbind(overlay, 'click', overlayClickHandler);
|
||||
unbind(overlay, 'wheel', overlayWheelHandler);
|
||||
unbind(btnPrev, 'click', showPreviousImage);
|
||||
unbind(btnNext, 'click', showNextImage);
|
||||
unbind(btnPrev, 'click', showLeftImage);
|
||||
unbind(btnNext, 'click', showRightImage);
|
||||
unbind(btnClose, 'click', hideOverlay);
|
||||
unbind(btnVmode, 'click', tglVmode);
|
||||
unbind(btnHelp, 'click', halp);
|
||||
unbind(btnAnim, 'click', anim);
|
||||
unbind(btnReadDir, 'click', toggleReadDir);
|
||||
unbind(btnRotL, 'click', rotl);
|
||||
unbind(btnRotR, 'click', rotr);
|
||||
unbind(btnSel, 'click', tglsel);
|
||||
|
@ -558,6 +591,23 @@ window.baguetteBox = (function () {
|
|||
btnAnim.textContent = ['⇄', '⮺', '⚡'][anims.indexOf(an)];
|
||||
btnAnim.setAttribute('tt', 'animation: ' + an);
|
||||
|
||||
options.readDirRtl = sread('greaddir') === "rtl";
|
||||
var msg;
|
||||
if (options.readDirRtl) {
|
||||
btnReadDir.innerText = "rtl";
|
||||
msg = "browse from right to left";
|
||||
slider.style.display = "flex";
|
||||
slider.style.flexDirection = "row-reverse";
|
||||
} else {
|
||||
btnReadDir.innerText = "ltr";
|
||||
msg = "browse from left to right";
|
||||
slider.style.flexDirection = "";
|
||||
slider.style.display = "block";
|
||||
}
|
||||
btnReadDir.setAttribute("tt", msg);
|
||||
btnReadDir.setAttribute("aria-label", msg);
|
||||
|
||||
|
||||
slider.style.transition = (options.animation === 'fadeIn' ? 'opacity .3s ease' :
|
||||
options.animation === 'slideIn' ? '' : 'none');
|
||||
|
||||
|
@ -778,8 +828,7 @@ window.baguetteBox = (function () {
|
|||
image.setAttribute('playsinline', '1');
|
||||
// ios ignores poster
|
||||
image.onended = vidEnd;
|
||||
image.onplay = function () { show_buttons(1); };
|
||||
image.onpause = function () { show_buttons(); };
|
||||
image.onplay = image.onpause = ppHandler;
|
||||
}
|
||||
image.alt = thumbnailElement ? thumbnailElement.alt || '' : '';
|
||||
if (options.titleTag && imageCaption)
|
||||
|
@ -794,12 +843,33 @@ window.baguetteBox = (function () {
|
|||
callback();
|
||||
}
|
||||
|
||||
function showNextImage(e) {
|
||||
function ppHandler() {
|
||||
var now = Date.now();
|
||||
if (now - un_pp < 50) {
|
||||
un_pp = 0;
|
||||
return playpause(); // browser undid space hotkey
|
||||
}
|
||||
show_buttons(this.paused ? 1 : 0);
|
||||
}
|
||||
|
||||
function showRightImage(e) {
|
||||
ev(e);
|
||||
var dir = options.readDirRtl ? -1 : 1;
|
||||
return show(currentIndex + dir);
|
||||
}
|
||||
|
||||
function showLeftImage(e) {
|
||||
ev(e);
|
||||
var dir = options.readDirRtl ? 1 : -1;
|
||||
return show(currentIndex + dir);
|
||||
}
|
||||
|
||||
function showNextImageIgnoreReadDir(e) {
|
||||
ev(e);
|
||||
return show(currentIndex + 1);
|
||||
}
|
||||
|
||||
function showPreviousImage(e) {
|
||||
function showPreviousImageIgnoreReadDir(e) {
|
||||
ev(e);
|
||||
return show(currentIndex - 1);
|
||||
}
|
||||
|
@ -827,10 +897,10 @@ window.baguetteBox = (function () {
|
|||
}
|
||||
|
||||
if (index < 0)
|
||||
return bounceAnimation('left');
|
||||
return bounceAnimation(options.readDirRtl ? 'right' : 'left');
|
||||
|
||||
if (index >= imagesElements.length)
|
||||
return bounceAnimation('right');
|
||||
return bounceAnimation(options.readDirRtl ? 'left' : 'right');
|
||||
|
||||
try {
|
||||
vid().pause();
|
||||
|
@ -984,7 +1054,7 @@ window.baguetteBox = (function () {
|
|||
|
||||
function vidEnd() {
|
||||
if (this == vid() && vnext)
|
||||
showNextImage();
|
||||
showNextImageIgnoreReadDir();
|
||||
}
|
||||
|
||||
function setloop(side) {
|
||||
|
@ -1045,11 +1115,12 @@ window.baguetteBox = (function () {
|
|||
return false;
|
||||
}
|
||||
|
||||
function updateOffset() {
|
||||
var offset = -currentIndex * 100 + '%',
|
||||
function updateOffset(noTransition) {
|
||||
var dir = options.readDirRtl ? 1 : -1,
|
||||
offset = dir * currentIndex * 100 + '%',
|
||||
xform = slider.style.perspective !== undefined;
|
||||
|
||||
if (options.animation === 'fadeIn') {
|
||||
if (options.animation === 'fadeIn' && !noTransition) {
|
||||
slider.style.opacity = 0;
|
||||
setTimeout(function () {
|
||||
xform ?
|
||||
|
@ -1095,10 +1166,10 @@ window.baguetteBox = (function () {
|
|||
fx = x / (rc.right - rc.left);
|
||||
|
||||
if (fx < 0.3)
|
||||
return showPreviousImage();
|
||||
return showLeftImage();
|
||||
|
||||
if (fx > 0.7)
|
||||
return showNextImage();
|
||||
return showRightImage();
|
||||
|
||||
show_buttons('t');
|
||||
|
||||
|
@ -1154,8 +1225,8 @@ window.baguetteBox = (function () {
|
|||
return {
|
||||
run: run,
|
||||
show: show,
|
||||
showNext: showNextImage,
|
||||
showPrevious: showPreviousImage,
|
||||
showNext: showRightImage,
|
||||
showPrevious: showLeftImage,
|
||||
relseek: relseek,
|
||||
urltime: urltime,
|
||||
playpause: playpause,
|
||||
|
|
|
@ -84,6 +84,13 @@
|
|||
--sort-1: #fb0;
|
||||
--sort-2: #d09;
|
||||
|
||||
--sz-b: #aaa;
|
||||
--sz-k: #4ff;
|
||||
--sz-m: var(--tab-alt);
|
||||
--sz-g: var(--a);
|
||||
--sz-t: var(--sz-g);
|
||||
--sz-p: var(--sz-t);
|
||||
|
||||
--srv-1: #aaa;
|
||||
--srv-2: #a73;
|
||||
--srv-3: #f4c;
|
||||
|
@ -187,6 +194,9 @@ html.y {
|
|||
--sort-1: #059;
|
||||
--sort-2: #f5d;
|
||||
|
||||
--sz-b: #777;
|
||||
--sz-k: #380;
|
||||
|
||||
--srv-1: #555;
|
||||
--srv-2: #c83;
|
||||
--srv-3: #c0a;
|
||||
|
@ -344,6 +354,9 @@ html.cz {
|
|||
--btn-1-bb: .2em solid #e90;
|
||||
--btn-1-bs: 0 .1em .8em rgba(255,205,0,0.9);
|
||||
|
||||
--sz-b: #ddd;
|
||||
--sz-k: #c9f;
|
||||
|
||||
--srv-3: #fff;
|
||||
|
||||
--u2-tab-b1: var(--bg-d3);
|
||||
|
@ -571,6 +584,7 @@ pre, code, tt, #doc, #doc>code {
|
|||
overflow: hidden;
|
||||
width: 0;
|
||||
height: 0;
|
||||
left: -10em;
|
||||
color: var(--bg);
|
||||
}
|
||||
html .ayjump:focus {
|
||||
|
@ -739,6 +753,15 @@ html.y #files tr.fade a {
|
|||
#files tbody tr td:last-child {
|
||||
white-space: nowrap;
|
||||
}
|
||||
#files span.fsz_B { color: var(--sz-b); }
|
||||
#files span.fsz_K { color: var(--sz-k); }
|
||||
#files span.fsz_M { color: var(--sz-m); }
|
||||
#files span.fsz_G { color: var(--sz-g); }
|
||||
#files span.fsz_T { color: var(--sz-t); }
|
||||
#files span.fsz_P { color: var(--sz-p); }
|
||||
html.y #files span.fsz_G,
|
||||
html.y #files span.fsz_T,
|
||||
html.y #files span.fsz_P { font-weight: bold }
|
||||
#files thead th[style] {
|
||||
width: auto !important;
|
||||
}
|
||||
|
@ -864,6 +887,9 @@ html.y #path a:hover {
|
|||
#srv_info2 span {
|
||||
color: var(--srv-1);
|
||||
}
|
||||
#srv_info2 a {
|
||||
padding: 0;
|
||||
}
|
||||
#srv_info2 {
|
||||
display: none;
|
||||
}
|
||||
|
@ -881,6 +907,9 @@ html.y #path a:hover {
|
|||
#flogout {
|
||||
display: inline;
|
||||
}
|
||||
html.dz #flogout {
|
||||
margin-left: 1em;
|
||||
}
|
||||
#goh+span {
|
||||
color: var(--bg-u5);
|
||||
padding-left: .5em;
|
||||
|
@ -1151,6 +1180,9 @@ html.y #widget.open {
|
|||
border: 1px solid var(--bg-u5);
|
||||
border-width: 0 .1em 0 0;
|
||||
}
|
||||
#wzip1 {
|
||||
margin-right: .2em;
|
||||
}
|
||||
#wfm.act+#wzip1+#wzip,
|
||||
#wfm.act+#wzip1+#wzip+#wnp {
|
||||
margin-left: .2em;
|
||||
|
@ -1554,11 +1586,13 @@ html {
|
|||
#treepar {
|
||||
z-index: 1;
|
||||
position: fixed;
|
||||
background: #fff;
|
||||
background: var(--tree-bg);
|
||||
left: -.96em;
|
||||
width: calc(.3em + var(--nav-sz) - var(--sbw));
|
||||
border-bottom: 1px solid var(--bg-u5);
|
||||
overflow: hidden;
|
||||
border-right: .5em solid #999\9;
|
||||
}
|
||||
#treepar.off {
|
||||
display: none;
|
||||
|
@ -1905,6 +1939,8 @@ html.y #tree.nowrap .ntree a+a:hover {
|
|||
#rui td input[type="text"] {
|
||||
width: 100%;
|
||||
}
|
||||
#rui #rn_n_d,
|
||||
#rui #rn_n_s,
|
||||
#shui td.exs input[type="text"] {
|
||||
width: 3em;
|
||||
}
|
||||
|
@ -1994,6 +2030,7 @@ html.y #doc .line-highlight {
|
|||
}
|
||||
#seldoc.sel {
|
||||
color: var(--fg2-max);
|
||||
background: #f0f;
|
||||
background: var(--g-sel-b1);
|
||||
}
|
||||
#pvol,
|
||||
|
@ -2013,6 +2050,7 @@ a.btn,
|
|||
user-select: none;
|
||||
}
|
||||
#hkhelp {
|
||||
background: #fff;
|
||||
background: var(--bg);
|
||||
}
|
||||
#hkhelp table {
|
||||
|
@ -2107,6 +2145,7 @@ html.noscroll .sbar::-webkit-scrollbar {
|
|||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
flex: none;
|
||||
}
|
||||
.full-image figure {
|
||||
display: inline;
|
||||
|
@ -2301,6 +2340,9 @@ html.y #bbox-overlay figcaption a {
|
|||
0%, 100% {transform: scale(0)}
|
||||
50% {transform: scale(1)}
|
||||
}
|
||||
.no-transition {
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
@ -3488,7 +3530,6 @@ html.ez {
|
|||
html.e {
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
html.e #files,
|
||||
html.e #u2conf input[type="checkbox"]:hover + label,
|
||||
html.e .tgl.btn.on:hover,
|
||||
|
@ -3588,6 +3629,10 @@ html.e #srv_info {
|
|||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
html.e #acc_info span.warn,
|
||||
html.e #acc_info a {
|
||||
color: var(--white);
|
||||
}
|
||||
html.e #flogout:before {
|
||||
padding-left: 0.2em;
|
||||
padding-right: 0.4em;
|
||||
|
|
|
@ -134,7 +134,6 @@
|
|||
CGV = {{ cgv|tojson }},
|
||||
TS = "{{ ts }}",
|
||||
dtheme = "{{ dtheme }}",
|
||||
srvinf = "{{ srv_info }}",
|
||||
lang = "{{ lang }}",
|
||||
dfavico = "{{ favico }}",
|
||||
have_tags_idx = {{ have_tags_idx }},
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -23,17 +23,17 @@
|
|||
<th>user</th>
|
||||
<th>groups</th>
|
||||
</tr></thead><tbody>
|
||||
{% for un, gn in rows %}
|
||||
{%- for un, gn in rows %}
|
||||
<tr>
|
||||
<td><a href="{{ r }}/?idp=rm={{ un|e }}">forget</a></td>
|
||||
<td>{{ un|e }}</td>
|
||||
<td>{{ gn|e }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{%- endfor %}
|
||||
</tbody></table>
|
||||
{% if not rows %}
|
||||
{%- if not rows %}
|
||||
(there are no IdP users in the cache)
|
||||
{% endif %}
|
||||
{%- endif %}
|
||||
</div>
|
||||
<a href="#" id="repl">π</a>
|
||||
<script>
|
||||
|
|
|
@ -130,7 +130,8 @@ write markdown (most html is 🙆 too)
|
|||
|
||||
var SR = "{{ r }}",
|
||||
last_modified = {{ lastmod }},
|
||||
have_emp = {{ "true" if have_emp else "false" }},
|
||||
have_emp = {{ have_emp }},
|
||||
md_no_br = {{ md_no_br }},
|
||||
dfavico = "{{ favico }}";
|
||||
|
||||
var md_opt = {
|
||||
|
|
|
@ -201,7 +201,7 @@ function convert_markdown(md_text, dest_dom) {
|
|||
|
||||
var marked_opts = {
|
||||
//headerPrefix: 'h-',
|
||||
breaks: true,
|
||||
breaks: !md_no_br,
|
||||
gfm: true
|
||||
};
|
||||
|
||||
|
@ -217,7 +217,7 @@ function convert_markdown(md_text, dest_dom) {
|
|||
catch (ex) {
|
||||
if (IE) {
|
||||
dest_dom.innerHTML = 'IE cannot into markdown ;_;';
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ext)
|
||||
|
@ -344,6 +344,8 @@ function convert_markdown(md_text, dest_dom) {
|
|||
}
|
||||
catch (ex) { }
|
||||
}, 1);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
@ -422,7 +424,7 @@ function init_toc() {
|
|||
}
|
||||
}
|
||||
|
||||
// hilight the correct toc items + scroll into view
|
||||
// highlight the correct toc items + scroll into view
|
||||
function freshen_toclist() {
|
||||
if (anchors.length == 0)
|
||||
return;
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
"use strict";
|
||||
|
||||
|
||||
var sloc0 = '' + location,
|
||||
dbg_kbd = /[?&]dbgkbd\b/.exec(sloc0);
|
||||
|
||||
|
||||
// server state
|
||||
var server_md = dom_src.value;
|
||||
|
||||
|
@ -697,8 +701,14 @@ function reLastIndexOf(txt, ptn, end) {
|
|||
// table formatter
|
||||
function fmt_table(e) {
|
||||
if (e) e.preventDefault();
|
||||
//dom_tbox.className = '';
|
||||
|
||||
try {
|
||||
fmt_table2();
|
||||
}
|
||||
catch (ex) {
|
||||
return toast.err(7, 'table-format (CTRL-K) failed:\n' + ex);
|
||||
}
|
||||
}
|
||||
function fmt_table2() {
|
||||
var txt = dom_src.value,
|
||||
ofs = dom_src.selectionStart,
|
||||
//o0 = txt.lastIndexOf('\n\n', ofs),
|
||||
|
@ -930,23 +940,30 @@ var set_lno = (function () {
|
|||
|
||||
// hotkeys / toolbar
|
||||
(function () {
|
||||
var keydown = function (ev) {
|
||||
if (!ev && window.event) {
|
||||
ev = window.event;
|
||||
var keydown = function (e) {
|
||||
if (!e && window.event) {
|
||||
e = window.event;
|
||||
if (dev_fbw == 1) {
|
||||
toast.warn(10, 'hello from fallback code ;_;\ncheck console trace');
|
||||
console.error('using window.event');
|
||||
}
|
||||
}
|
||||
var kc = ev.code || ev.keyCode || ev.which,
|
||||
var k = (e.key || e.code) + '',
|
||||
editing = document.activeElement == dom_src;
|
||||
|
||||
//console.log(ev.key, ev.code, ev.keyCode, ev.which);
|
||||
if (ctrl(ev) && (ev.code == "KeyS" || kc == 83)) {
|
||||
if (k.startsWith('Key'))
|
||||
k = k.slice(3);
|
||||
|
||||
var kl = k.toLowerCase();
|
||||
|
||||
if (dbg_kbd)
|
||||
console.log('KBD', k, kl, e.key, e.code, e.keyCode, e.which);
|
||||
|
||||
if (ctrl(e) && kl == "s") {
|
||||
save();
|
||||
return false;
|
||||
}
|
||||
if (ev.code == "Escape" || kc == 27) {
|
||||
if (k == "Escape" || k == "Esc") {
|
||||
var d = ebi('helpclose');
|
||||
if (d)
|
||||
d.click();
|
||||
|
@ -954,46 +971,44 @@ var set_lno = (function () {
|
|||
if (editing)
|
||||
set_lno();
|
||||
|
||||
if (ctrl(ev)) {
|
||||
if (ev.code == "KeyE") {
|
||||
if (ctrl(e)) {
|
||||
if (kl == "e") {
|
||||
dom_nsbs.click();
|
||||
return false;
|
||||
}
|
||||
if (!editing)
|
||||
return true;
|
||||
|
||||
if (ev.code == "KeyH" || kc == 72) {
|
||||
md_header(ev.shiftKey);
|
||||
if (kl == "h") {
|
||||
md_header(e.shiftKey);
|
||||
return false;
|
||||
}
|
||||
if (ev.code == "KeyZ" || kc == 90) {
|
||||
if (ev.shiftKey)
|
||||
if (kl == "z") {
|
||||
if (e.shiftKey)
|
||||
action_stack.redo();
|
||||
else
|
||||
action_stack.undo();
|
||||
|
||||
return false;
|
||||
}
|
||||
if (ev.code == "KeyY" || kc == 89) {
|
||||
if (kl == "y") {
|
||||
action_stack.redo();
|
||||
return false;
|
||||
}
|
||||
if (ev.code == "KeyK") {
|
||||
if (kl == "k") {
|
||||
fmt_table();
|
||||
return false;
|
||||
}
|
||||
if (ev.code == "KeyU") {
|
||||
if (kl == "u") {
|
||||
iter_uni();
|
||||
return false;
|
||||
}
|
||||
var up = ev.code == "ArrowUp" || kc == 38;
|
||||
var dn = ev.code == "ArrowDown" || kc == 40;
|
||||
if (up || dn) {
|
||||
md_p_jump(dn);
|
||||
if (k == "ArrowUp" || k == "ArrowDown") {
|
||||
md_p_jump(k == "ArrowDown");
|
||||
return false;
|
||||
}
|
||||
if (ev.code == "KeyX" || ev.code == "KeyC") {
|
||||
md_cut(ev.code == "KeyX");
|
||||
if (kl == "x" || kl == "c") {
|
||||
md_cut(kl == "x");
|
||||
return true; //sic
|
||||
}
|
||||
}
|
||||
|
@ -1001,18 +1016,18 @@ var set_lno = (function () {
|
|||
if (!editing)
|
||||
return true;
|
||||
|
||||
if (ev.code == "Tab" || kc == 9) {
|
||||
md_indent(ev.shiftKey);
|
||||
if (k == "Tab") {
|
||||
md_indent(e.shiftKey);
|
||||
return false;
|
||||
}
|
||||
if (ev.code == "Home" || kc == 36) {
|
||||
md_home(ev.shiftKey);
|
||||
if (k == "Home") {
|
||||
md_home(e.shiftKey);
|
||||
return false;
|
||||
}
|
||||
if (!ev.shiftKey && ((ev.code + '').endsWith("Enter") || kc == 13)) {
|
||||
if (!e.shiftKey && k.endsWith("Enter")) {
|
||||
return md_newline();
|
||||
}
|
||||
if (!ev.shiftKey && kc == 8) {
|
||||
if (!e.shiftKey && k == "Backspace") {
|
||||
return md_backspace();
|
||||
}
|
||||
}
|
||||
|
@ -1036,7 +1051,9 @@ ebi('help').onclick = function (e) {
|
|||
var dom = ebi('helpbox');
|
||||
var dtxt = dom.getElementsByTagName('textarea');
|
||||
if (dtxt.length > 0) {
|
||||
convert_markdown(dtxt[0].value, dom);
|
||||
var txt = dtxt[0].value;
|
||||
if (!convert_markdown(txt, dom))
|
||||
dom.innerText = txt.split('## markdown')[0];
|
||||
dom.innerHTML = '<a href="#" id="helpclose">close</a>' + dom.innerHTML;
|
||||
}
|
||||
|
||||
|
|
|
@ -28,7 +28,8 @@
|
|||
|
||||
var SR = "{{ r }}",
|
||||
last_modified = {{ lastmod }},
|
||||
have_emp = {{ "true" if have_emp else "false" }},
|
||||
have_emp = {{ have_emp }},
|
||||
md_no_br = {{ md_no_br }},
|
||||
dfavico = "{{ favico }}";
|
||||
|
||||
var md_opt = {
|
||||
|
|
31
copyparty/web/opds.xml
Normal file
31
copyparty/web/opds.xml
Normal file
|
@ -0,0 +1,31 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||
{%- for d in dirs %}
|
||||
<entry>
|
||||
<title>{{ d.name }}</title>
|
||||
<link rel="subsection"
|
||||
href="{{ d.href | e }}"
|
||||
type="application/atom+xml;profile=opds-catalog"/>
|
||||
<updated>{{ d.iso8601 }}</updated>
|
||||
</entry>
|
||||
{%- endfor %}
|
||||
{%- for f in files %}
|
||||
<entry>
|
||||
<title>{{ f.name }}</title>
|
||||
<updated>{{ f.iso8601 }}</updated>
|
||||
<link rel="http://opds-spec.org/acquisition"
|
||||
href="{{ f.href | e }}"
|
||||
type="{{ f.mime }}"/>
|
||||
{%- if f.jpeg_thumb_href != None %}
|
||||
<link rel="http://opds-spec.org/image/thumbnail"
|
||||
href="{{ f.jpeg_thumb_href | e }}"
|
||||
type="image/jpeg"/>
|
||||
{%- endif %}
|
||||
{%- if f.jpeg_thumb_href_hires != None %}
|
||||
<link rel="http://opds-spec.org/image"
|
||||
href="{{ f.jpeg_thumb_href_hires | e }}"
|
||||
type="image/jpeg"/>
|
||||
{%- endif %}
|
||||
</entry>
|
||||
{%- endfor %}
|
||||
</feed>
|
|
@ -36,7 +36,7 @@
|
|||
<th>hrs</th>
|
||||
<th>add time</th>
|
||||
</tr></thead><tbody>
|
||||
{% for k, pw, vp, pr, st, un, t0, t1 in rows %}
|
||||
{%- for k, pw, vp, pr, st, un, t0, t1 in rows %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ r }}{{ shr }}{{ k }}?qr">qr</a>
|
||||
|
@ -54,11 +54,11 @@
|
|||
<td>{{ "inf" if not t1 else "dead" if t1 < now else ((t1 - now) / 3600) | round(1) }}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{%- endfor %}
|
||||
</tbody></table>
|
||||
{% if not rows %}
|
||||
{%- if not rows %}
|
||||
(you don't have any active shares btw)
|
||||
{% endif %}
|
||||
{%- endif %}
|
||||
</div>
|
||||
<a href="#" id="repl">π</a>
|
||||
<script>
|
||||
|
|
|
@ -38,6 +38,7 @@ a {
|
|||
td a {
|
||||
margin: 0;
|
||||
}
|
||||
#wb,
|
||||
#w {
|
||||
color: #fff;
|
||||
background: #940;
|
||||
|
@ -117,6 +118,26 @@ table {
|
|||
.btns>a:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
.agr br {
|
||||
display: none;
|
||||
}
|
||||
#lo,
|
||||
.agr a,
|
||||
.agr form {
|
||||
margin: 0 .5em 0 0;
|
||||
line-height: 4em;
|
||||
}
|
||||
.agr form,
|
||||
.agr input {
|
||||
display: inline;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
#lo,
|
||||
.agr input {
|
||||
line-height: 1em;
|
||||
font-weight: normal;
|
||||
}
|
||||
#msg {
|
||||
margin: 3em 0;
|
||||
}
|
||||
|
|
|
@ -18,12 +18,16 @@
|
|||
<a id="a" href="{{ r }}/?h{{ re }}" class="af">refresh</a>
|
||||
<a id="v" href="{{ r }}/?hc" class="af">connect</a>
|
||||
|
||||
{%- if this.uname == '*' %}
|
||||
<p id="b">howdy stranger <small>(you're not logged in)</small></p>
|
||||
{%- else %}
|
||||
<a id="c" href="{{ r }}/?pw=x" class="logout">logout</a>
|
||||
<p><span id="m">welcome back,</span> <strong id="un">{{ this.uname|e }}</strong></p>
|
||||
{%- endif %}
|
||||
{%- if this.uname == '*' %}
|
||||
<p id="b">howdy stranger <small>(you're not logged in)</small></p>
|
||||
{%- else %}
|
||||
{%- if this.args.idp_logout %}
|
||||
<a id="c" href="{{ this.args.idp_logout }}" class="logout">logout</a>
|
||||
{%- else %}
|
||||
<a id="c" href="{{ r }}/?pw=x" class="logout">logout</a>
|
||||
{%- endif %}
|
||||
<p><span id="m">welcome back,</span> <strong id="un">{{ this.uname|e }}</strong></p>
|
||||
{%- endif %}
|
||||
{%- endif %}
|
||||
|
||||
{%- if msg %}
|
||||
|
@ -37,9 +41,9 @@
|
|||
<table class="vols">
|
||||
<thead><tr><th>%</th><th>speed</th><th>eta</th><th>idle</th><th>dir</th><th>file</th></tr></thead>
|
||||
<tbody>
|
||||
{% for u in ups %}
|
||||
{%- for u in ups %}
|
||||
<tr><td>{{ u[0] }}</td><td>{{ u[1] }}</td><td>{{ u[2] }}</td><td>{{ u[3] }}</td><td><a href="{{ u[4] }}">{{ u[5]|e }}</a></td><td>{{ u[6]|e }}</td></tr>
|
||||
{% endfor %}
|
||||
{%- endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{%- endif %}
|
||||
|
@ -49,9 +53,9 @@
|
|||
<table class="vols">
|
||||
<thead><tr><th>%</th><th>sent</th><th>speed</th><th>eta</th><th>idle</th><th></th><th>dir</th><th>file</th></tr></thead>
|
||||
<tbody>
|
||||
{% for u in dls %}
|
||||
{%- for u in dls %}
|
||||
<tr><td>{{ u[0] }}</td><td>{{ u[1] }}</td><td>{{ u[2] }}</td><td>{{ u[3] }}</td><td>{{ u[4] }}</td><td>{{ u[5] }}</td><td><a href="{{ u[6] }}">{{ u[7]|e }}</a></td><td>{{ u[8] }}</td></tr>
|
||||
{% endfor %}
|
||||
{%- endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{%- endif %}
|
||||
|
@ -70,11 +74,11 @@
|
|||
<table class="vols">
|
||||
<thead><tr><th>vol</th><th id="t">action</th><th>status</th></tr></thead>
|
||||
<tbody>
|
||||
{% for mp in avol %}
|
||||
{%- for mp in avol %}
|
||||
{%- if mp in vstate and vstate[mp] %}
|
||||
<tr><td><a href="{{ r }}{{ mp }}{{ url_suf }}">{{ mp }}</a></td><td><a class="s" href="{{ r }}{{ mp }}?scan">rescan</a></td><td>{{ vstate[mp] }}</td></tr>
|
||||
{%- endif %}
|
||||
{% endfor %}
|
||||
{%- endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</td></tr></table>
|
||||
|
@ -87,18 +91,18 @@
|
|||
{%- if rvol %}
|
||||
<h1 id="f">you can browse:</h1>
|
||||
<ul>
|
||||
{% for mp in rvol %}
|
||||
{%- for mp in rvol %}
|
||||
<li><a href="{{ r }}{{ mp }}{{ url_suf }}">{{ mp }}</a></li>
|
||||
{% endfor %}
|
||||
{%- endfor %}
|
||||
</ul>
|
||||
{%- endif %}
|
||||
|
||||
{%- if wvol %}
|
||||
<h1 id="g">you can upload to:</h1>
|
||||
<ul>
|
||||
{% for mp in wvol %}
|
||||
{%- for mp in wvol %}
|
||||
<li><a href="{{ r }}{{ mp }}{{ url_suf }}">{{ mp }}</a></li>
|
||||
{% endfor %}
|
||||
{%- endfor %}
|
||||
</ul>
|
||||
{%- endif %}
|
||||
|
||||
|
@ -110,71 +114,85 @@
|
|||
<input type="password" id="lp" name="cppwd" placeholder=" password" />
|
||||
<input type="hidden" name="uhash" id="uhash" value="x" />
|
||||
<input type="submit" id="ls" value="Unlock" />
|
||||
{% if ahttps %}
|
||||
{%- if ahttps %}
|
||||
<a id="w" href="{{ ahttps }}">switch to https</a>
|
||||
{% endif %}
|
||||
{%- endif %}
|
||||
</form>
|
||||
</div>
|
||||
{%- else %}
|
||||
<h1 id="l">login for more:</h1>
|
||||
<div>
|
||||
{%- if this.args.idp_login %}
|
||||
<ul><li>
|
||||
<a href="{{ this.args.idp_login | replace("{dst}",r+"/"+qvpath) }}">{{ this.args.idp_login_t }}</a>
|
||||
{%- if this.args.ao_have_pw %}or alternatively:{%- endif %}
|
||||
</li></ul>
|
||||
{%- endif %}
|
||||
{%- if this.args.ao_have_pw %}
|
||||
<form id="lf" method="post" enctype="multipart/form-data" action="{{ r }}/{{ qvpath }}">
|
||||
<input type="hidden" id="la" name="act" value="login" />
|
||||
{% if this.args.usernames %}
|
||||
{%- if this.args.usernames %}
|
||||
<input type="text" id="lu" name="uname" placeholder=" username" size="12" />
|
||||
<input type="password" id="lp" name="cppwd" placeholder=" password" size="12" />
|
||||
{% else %}
|
||||
{%- else %}
|
||||
<input type="password" id="lp" name="cppwd" placeholder=" password" />
|
||||
{% endif %}
|
||||
{%- endif %}
|
||||
<input type="hidden" name="uhash" id="uhash" value="x" />
|
||||
<input type="submit" id="ls" value="login" />
|
||||
{% if chpw %}
|
||||
{%- if chpw %}
|
||||
<a id="x" href="#">change password</a>
|
||||
{% endif %}
|
||||
{% if ahttps %}
|
||||
{%- endif %}
|
||||
{%- if ahttps %}
|
||||
<a id="w" href="{{ ahttps }}">switch to https</a>
|
||||
{% endif %}
|
||||
{%- endif %}
|
||||
</form>
|
||||
{%- endif %}
|
||||
</div>
|
||||
{%- endif %}
|
||||
|
||||
<h1 id="cc">other stuff:</h1>
|
||||
<ul>
|
||||
{%- if this.uname in this.args.idp_adm_set %}
|
||||
<li><a id="ag" href="{{ r }}/?idp">view idp cache</a></li>
|
||||
{% endif %}
|
||||
<div class="agr">
|
||||
{%- if ahttps %}
|
||||
<a id="wb" href="{{ ahttps }}">switch to https</a><br />
|
||||
{%- endif %}
|
||||
|
||||
<a id="af" href="{{ r }}/?ru">show recent uploads</a><br />
|
||||
|
||||
{%- if this.uname != '*' and this.args.shr %}
|
||||
<li><a id="y" href="{{ r }}/?shares">edit shares</a></li>
|
||||
{% endif %}
|
||||
<a id="y" href="{{ r }}/?shares">edit shares</a><br />
|
||||
{%- endif %}
|
||||
|
||||
{% if k304 or k304vis %}
|
||||
{% if k304 %}
|
||||
{%- if this.uname in this.args.idp_adm_set %}
|
||||
<a id="ag" href="{{ r }}/?idp">view idp cache</a><br />
|
||||
{%- endif %}
|
||||
|
||||
<a id="k" href="{{ r }}/?reset" class="r" onclick="localStorage.clear();return true">reset client settings</a><br />
|
||||
|
||||
{%- if this.uname != '*' and not in_shr %}
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" name="act" value="logout" />
|
||||
<input type="submit" id="lo" value="logout “{{ this.uname|e }}” everywhere" />
|
||||
</form>
|
||||
{%- endif %}
|
||||
</div>
|
||||
<ul>
|
||||
{%- if k304 or k304vis %}
|
||||
{%- if k304 %}
|
||||
<li><a id="h" href="{{ r }}/?cc&setck=k304=n">disable k304</a> (currently enabled)
|
||||
{%- else %}
|
||||
<li><a id="i" href="{{ r }}/?cc&setck=k304=y" class="r">enable k304</a> (currently disabled)
|
||||
{% endif %}
|
||||
{%- endif %}
|
||||
<blockquote id="j">enabling k304 will disconnect your client on every HTTP 304, which can prevent some buggy proxies from getting stuck (suddenly not loading pages), <em>but</em> it will also make things slower in general</blockquote></li>
|
||||
{% endif %}
|
||||
{%- endif %}
|
||||
|
||||
{% if no304 or no304vis %}
|
||||
{% if no304 %}
|
||||
{%- if no304 or no304vis %}
|
||||
{%- if no304 %}
|
||||
<li><a id="ab" href="{{ r }}/?cc&setck=no304=n">disable no304</a> (currently enabled)
|
||||
{%- else %}
|
||||
<li><a id="ac" href="{{ r }}/?cc&setck=no304=y" class="r">enable no304</a> (currently disabled)
|
||||
{% endif %}
|
||||
{%- endif %}
|
||||
<blockquote id="ad">enabling no304 will disable all caching; try this if k304 wasn't enough. This will waste a huge amount of network traffic!</blockquote></li>
|
||||
{% endif %}
|
||||
|
||||
<li><a id="af" href="{{ r }}/?ru">show recent uploads</a></li>
|
||||
<li><a id="k" href="{{ r }}/?reset" class="r" onclick="localStorage.clear();return true">reset client settings</a></li>
|
||||
|
||||
{%- if this.uname != '*' %}
|
||||
<li><form method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" name="act" value="logout" />
|
||||
<input type="submit" id="lo" value="logout “{{ this.uname|e }}” everywhere" />
|
||||
</form></li>
|
||||
{% endif %}
|
||||
{%- endif %}
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
|
|
@ -197,6 +197,53 @@ var Ls = {
|
|||
"ae1": "Aktive Downloads:",
|
||||
"af1": "Zeige neue Uploads",
|
||||
},
|
||||
"epo": {
|
||||
"a1": "reŝargi",
|
||||
"b1": "sal, nekonatulo <small>(vi ne estas ensalutita)</small>",
|
||||
"c1": "elsaluti",
|
||||
"d1": "montru stakon", // TLNote: "d2" is the tooltip for this button
|
||||
"d2": "montras la staton de ĉiuj aktivaj fadenoj",
|
||||
"e1": "reŝargi CFGon",
|
||||
"e2": "reŝargas la agordo-dosierojn (kontoj/portiloj/portilo-flagoj),$Nkaj reskanas ĉiuj portiloj de ed2s$N$Nnoto: ĉiuj ŝanĝoj de ĝeneralaj agordoj postulas$Npostulas tutan restartigon por efektiviĝi",
|
||||
"f1": "vi povas vidi:",
|
||||
"g1": "vi povas alŝuti al:",
|
||||
"cc1": "aliaĵoj:",
|
||||
"h1": "malŝalti k304-on", // TLNote: "j1" explains what k304 is
|
||||
"i1": "ŝalti k304-on",
|
||||
"j1": "k304 malkonektas vian klienton je ĉiu HTTP-eraro 304; tio povas eviti paraliziĝon dum uzado de difektitaj retperantoj (paĝoj subite ne ŝargiĝas), <em>sed</em> ĝi ankaŭ plimalrapidigas ĉion",
|
||||
"k1": "rekomenci agordojn de kliento",
|
||||
"l1": "ensaluti por pli da opcioj:",
|
||||
"ls3": "ensaluti", //m
|
||||
"lu4": "uzantnomo", //m
|
||||
"lp4": "pasvorto", //m
|
||||
"lo3": "ensaluti kiel “{0}” ĉie", //m
|
||||
"lo2": "ĉi tiu finigos seancon en ĉiuj retumiloj", //m
|
||||
"m1": "bonvenon denove,", // TLNote: "welcome back, USERNAME"
|
||||
"n1": "404 ne trovita ┐( ´ -`)┌",
|
||||
"o1": 'aŭ eble vi ne havas rajton -- provu uzi pasvorton aŭ <a href="' + SR + '/?h">iri hejmen</a>',
|
||||
"p1": "403 ne permesita ~┻━┻",
|
||||
"q1": 'uzu pasvorton aŭ <a href="' + SR + '/?h">iru hejmen</a>',
|
||||
"r1": "hejmen",
|
||||
".s1": "reskani",
|
||||
"t1": "ago", // TLNote: this is the header above the "rescan" buttons
|
||||
"u2": "tempo post lasta skribo (alŝuto / alinomado / ...) je servilo$N( upload / rename / ... )$N$N17d = 17 tagoj$N1h23 = 1 horo 23 minutoj$N4m56 = 4 minutoj 56 sekundoj",
|
||||
"v1": "konekti",
|
||||
"v2": "uzi ĉi tiun servilon kiel loka disko",
|
||||
"w1": "uzi HTTPS-protokolon",
|
||||
"x1": "ŝanĝi pasvorton",
|
||||
"y1": "redakti komunaĵojn", // TLNote: shows the list of folders that the user has decided to share
|
||||
"z1": "malŝlosi ĉi tiun komunaĵon:", // TLNote: the password prompt to see a hidden share
|
||||
"ta1": "entajpu novan pasvorton unue",
|
||||
"ta2": "retajpu por konfirmi:",
|
||||
"ta3": "tajpo-eraro; bonvolu provu denove",
|
||||
"aa1": "aktivaj alŝutoj:",
|
||||
"ab1": "malŝalti no304-on",
|
||||
"ac1": "ŝalti no304-on",
|
||||
"ad1": "no304 malŝaltas ĉiun kaŝmemoradon; provu ĉi tion, se k304 ne riparis la difektojn. Ĉi tiu agordo malŝparas multon da datumtrafiko!",
|
||||
"ae1": "aktivaj elŝutoj:",
|
||||
"af1": "montri lastajn alŝutojn",
|
||||
"ag1": "montri kaŝmemoron de idp",
|
||||
},
|
||||
"fin": {
|
||||
"a1": "päivitä",
|
||||
"b1": "hei sie muukalainen <small>(et ole kirjautunut sisään)</small>",
|
||||
|
@ -571,6 +618,53 @@ var Ls = {
|
|||
"af1": "pokaż ostatnio przesłane pliki",
|
||||
"ag1": "pokaż znanych użytkowników IdP",
|
||||
},
|
||||
"por": {
|
||||
"a1": "atualizar",
|
||||
"b1": "olá <small>(você não está logado)</small>",
|
||||
"c1": "encerrar sessão",
|
||||
"d1": "despejar o estado da pilha",
|
||||
"d2": "mostra o estado de todos os threads ativos",
|
||||
"e1": "recarregar configuração",
|
||||
"e2": "recarregar arquivos de configuração (contas/volumes/indicadores de volume),$N e reescanear todos os volumes e2ds$N$Nnota: qualquer alteração na configuração global$N requer uma reinicialização completa para ter efeito",
|
||||
"f1": "você pode navegar:",
|
||||
"g1": "você pode fazer upload para:",
|
||||
"cc1": "outras coisas:",
|
||||
"h1": "desativar k304",
|
||||
"i1": "ativar k304",
|
||||
"j1": "ativar k304 irá desconectar seu cliente em cada HTTP 304, o que pode evitar que alguns proxies com erros fiquem presos (parando de carregar páginas de repente), <em>mas</em> também irá desacelerar as coisas em geral",
|
||||
"k1": "redefinir config. de cliente",
|
||||
"l1": "faça login para mais:",
|
||||
"ls3": "fazer login",
|
||||
"lu4": "nome de usuário",
|
||||
"lp4": "senha",
|
||||
"lo3": "encerrar sessão de \"{0}\" em todos os lugares",
|
||||
"lo2": "isso irá encerrar a sessão em todos os navegadores",
|
||||
"m1": "bem-vindo de volta,",
|
||||
"n1": "404 não encontrado ┐( ´ -`)┌",
|
||||
"o1": "ou talvez você não tenha acesso? -- tente com uma senha ou volte para o início",
|
||||
"p1": "403 proibido ~┻━┻",
|
||||
"q1": "use uma senha ou volte para o início",
|
||||
"r1": "ir para o início",
|
||||
".s1": "reescanear",
|
||||
"t1": "ação",
|
||||
"u2": "tempo desde a última gravação no servidor$N( upload / renomear / ... )$N$N17d = 17 dias$N1h23 = 1 hora 23 minutos$N4m56 = 4 minutos 56 segundos",
|
||||
"v1": "conectar",
|
||||
"v2": "usar este servidor como um disco rígido local",
|
||||
"w1": "mudar para https",
|
||||
"x1": "mudar senha",
|
||||
"y1": "editar recursos compartilhados",
|
||||
"z1": "desbloquear este recurso compartilhado:",
|
||||
"ta1": "primeiro digite sua nova senha",
|
||||
"ta2": "repita para confirmar a nova senha:",
|
||||
"ta3": "há um erro; por favor, tente novamente",
|
||||
"aa1": "arquivos de entrada:",
|
||||
"ab1": "desativar no304",
|
||||
"ac1": "ativar no304",
|
||||
"ad1": "ativar no304 irá desabilitar todo o armazenamento em cache; tente isso se k304 não for suficiente. Isso irá desperdiçar uma grande quantidade de tráfego de rede!",
|
||||
"ae1": "downloads ativos:",
|
||||
"af1": "mostrar uploads recentes",
|
||||
"ag1": "mostrar usuários IdP conhecidos"
|
||||
},
|
||||
"spa": {
|
||||
"a1": "actualizar",
|
||||
"b1": "hola <small>(no has iniciado sesión)</small>",
|
||||
|
@ -759,6 +853,47 @@ var Ls = {
|
|||
"af1": "показать недавние загрузки",
|
||||
"ag1": "показать известных IdP-пользователей",
|
||||
},
|
||||
"tur": {
|
||||
"a1": "yenile",
|
||||
"b1": "N'aber aga <small>(giriş yapmamışsın)</small>",
|
||||
"c1": "çıkış yap",
|
||||
"d1": "yığını yolla",
|
||||
"d2": "tüm aktif iş parçacıklarının durumunu gösterir",
|
||||
"e1": "cfg'yi yenile",
|
||||
"e2": "yapılandırma dosyalarını yenile (hesaplar/hacimler/hacim bayrakları),$Nve tüm e2ds hacimlerini yeniden tarayın$N$Nnot: global ayarlardaki herhangi bir değişiklik$Netkili hale gelmesi için tam bir yeniden başlatma gerektirir",
|
||||
"f1": "göz atabilirsiniz:",
|
||||
"g1": "yükleyebilirsiniz:",
|
||||
"cc1": "diğer şeyler:",
|
||||
"h1": "k304'ü devre dışı bırak",
|
||||
"i1": "k304'ü etkinleştir",
|
||||
"j1": "k304'ü etkinleştirmek, her HTTP 304'te istemcinizin bağlantısını keser, bu da bazı hatalı proxy'lerin takılmasını önleyebilir (sayfaların birdenbire yüklenmesinin durması gibi); <em>ama</em> bu, aynı zamanda genel olarak işleyişi yavaşlatır",
|
||||
"k1": "istemci ayarlarını sıfırla",
|
||||
"l1": "daha fazlası için giriş yap:",
|
||||
"m1": "hoş geldin,",
|
||||
"n1": "404 bulunamadı ┐( ´ -`)┌",
|
||||
"o1": 'ya da erişim iznin yok -- bir şifre dene veya <a href="' + SR + '/?h">ana sayfaya dön</a>',
|
||||
"p1": "403 yasaklandı ~┻━┻",
|
||||
"q1": 'bir şifre kullan veya <a href="' + SR + '/?h">ana sayfaya dön</a>',
|
||||
"r1": "ana sayfaya dön",
|
||||
".s1": "yeniden tara",
|
||||
"t1": "işlem",
|
||||
"u2": "son sunucu yazma zamanı$N( yükleme / yeniden adlandırma / ... )$N$N17d = 17 gün$N1h23 = 1 saat 23 dakika$N4m56 = 4 dakika 56 saniye",
|
||||
"v1": "bağlan",
|
||||
"v2": "bu sunucuyu yerel HDD olarak kullan",
|
||||
"w1": "https'ye geç",
|
||||
"x1": "şifreyi değiştir",
|
||||
"y1": "paylaşılanları düzenle",
|
||||
"z1": "gizli paylaşımın kilidini aç:",
|
||||
"ta1": "ilk önce yeni şifreyi doldur",
|
||||
"ta2": "yeni şifreyi onaylamak için tekrar girin:",
|
||||
"ta3": "bir yazım hatası bulundu; lütfen tekrar deneyin",
|
||||
"aa1": "gelen dosyalar:",
|
||||
"ab1": "no304'ü devre dışı bırak",
|
||||
"ac1": "no304'ü etkinleştir",
|
||||
"ad1": "no304'ü etkinleştirmek, tüm önbelleği devre dışı bırakır; bunu k304 yeterli olmadıysa deneyin. Bu, büyük miktarda ağ trafiği israf edecektir!",
|
||||
"ae1": "aktif indirmeler:",
|
||||
"af1": "son yüklemeleri göster",
|
||||
},
|
||||
};
|
||||
|
||||
if (window.langmod)
|
||||
|
@ -767,6 +902,8 @@ if (window.langmod)
|
|||
var d = Ls[sread("cpp_lang", Object.keys(Ls)) || lang] ||
|
||||
Ls.eng || Ls.nor || Ls.chi;
|
||||
|
||||
d.wb = d.w;
|
||||
|
||||
for (var k in (d || {})) {
|
||||
var f = k.slice(-1),
|
||||
i = k.slice(0, -1),
|
||||
|
@ -787,7 +924,7 @@ if (o1 && o2 && d.lo3)
|
|||
o1.setAttribute("value", d.lo3.format(o2.textContent));
|
||||
|
||||
try {
|
||||
if (is_idp) {
|
||||
if (is_idp > 1) {
|
||||
var z = ['#l+div', '#l', '#c'];
|
||||
for (var a = 0; a < z.length; a++)
|
||||
QS(z[a]).style.display = 'none';
|
||||
|
@ -797,14 +934,16 @@ catch (ex) { }
|
|||
|
||||
tt.init();
|
||||
var o = QS('input[name="uname"]') || QS('input[name="cppwd"]');
|
||||
if (!MOBILE && !ebi('c') && o.offsetTop + o.offsetHeight < window.innerHeight)
|
||||
if (o && !MOBILE && !ebi('c') && o.offsetTop + o.offsetHeight < window.innerHeight)
|
||||
o.focus();
|
||||
|
||||
o = ebi('u');
|
||||
if (o && /[0-9]+$/.exec(o.innerHTML))
|
||||
o.innerHTML = shumantime(o.innerHTML);
|
||||
|
||||
ebi('uhash').value = '' + location.hash;
|
||||
o = ebi('uhash')
|
||||
if (o)
|
||||
o.value = '' + location.hash;
|
||||
|
||||
if (/\&re=/.test('' + location))
|
||||
ebi('a').className = 'af g';
|
||||
|
|
|
@ -31,10 +31,10 @@
|
|||
<br />
|
||||
<span class="os win lin mac">placeholders:</span>
|
||||
<span class="os win">
|
||||
{% if accs %}<code><b id="pw0">{{ pw }}</b></code>=password, {% endif %}<code><b>W:</b></code>=mountpoint
|
||||
{% if accs %}{% if un %}<code><b id="un0">{{ un }}</b></code>=username, <code><b id="up0">{{ unpw }}</b></code>=username:password, {% endif %}<code><b id="pw0">{{ pw }}</b></code>=password, {% endif %}<code><b>W:</b></code>=mountpoint
|
||||
</span>
|
||||
<span class="os lin mac">
|
||||
{% if accs %}<code><b id="pw0">{{ pw }}</b></code>=password, {% endif %}<code><b>mp</b></code>=mountpoint
|
||||
{% if accs %}{% if un %}<code><b id="un0">{{ un }}</b></code>=username, <code><b id="up0">{{ unpw }}</b></code>=username:password, {% endif %}<code><b id="pw0">{{ pw }}</b></code>=password, {% endif %}<code><b>mp</b></code>=mountpoint
|
||||
</span>
|
||||
{% if accs %}<a href="#" id="setpw">use real password</a>{% endif %}
|
||||
<a href="#" id="qr">show qr</a>
|
||||
|
@ -44,7 +44,7 @@
|
|||
|
||||
{% if args.have_idp_hdrs %}
|
||||
<p style="line-height:2em"><b>WARNING:</b> this server is using IdP-based authentication, so this stuff may not work as advertised. Depending on server config, these commands can probably only be used to access areas which don't require authentication, unless you auth using any non-IdP accounts defined in the copyparty config. Please see <a href="https://github.com/9001/copyparty/blob/hovudstraum/docs/idp.md#connecting-webdav-clients">the IdP docs</a></p>
|
||||
{% endif %}
|
||||
{%- endif %}
|
||||
|
||||
|
||||
|
||||
|
@ -54,50 +54,64 @@
|
|||
<div class="os win">
|
||||
<p>if you can, install <a href="https://winfsp.dev/rel/">winfsp</a>+<a href="https://downloads.rclone.org/rclone-current-windows-amd64.zip">rclone</a> and then paste this in cmd:</p>
|
||||
<pre>
|
||||
rclone config create {{ aname }}-dav webdav url=http{{ s }}://{{ rip }}{{ hport }} vendor=owncloud pacer_min_sleep=0.01ms{% if accs %} user=k pass=<b>{{ pw }}</b>{% endif %}
|
||||
rclone config create {{ aname }}-dav webdav url=http{{ s }}://{{ rip }}{{ hport }} vendor=owncloud pacer_min_sleep=0.01ms{% if accs %} user={{ b_un }} pass=<b>{{ pw }}</b>{% endif %}
|
||||
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-dav:{{ rvp }} <b>W:</b>
|
||||
</pre>
|
||||
<ul>
|
||||
{% if s %}
|
||||
{%- if s %}
|
||||
<li>running <code>rclone mount</code> on LAN (or just dont have valid certificates)? add <code>--no-check-certificate</code></li>
|
||||
{% endif %}
|
||||
{%- endif %}
|
||||
<li>old version of rclone? replace all <code>=</code> with <code> </code> (space)</li>
|
||||
</ul>
|
||||
|
||||
<p>if you want to use the native WebDAV client in windows instead (slow and buggy), first run <a href="{{ r }}/.cpr/a/webdav-cfg.bat">webdav-cfg.bat</a> to remove the 47 MiB filesize limit (also fixes latency and password login), then connect:</p>
|
||||
<pre>
|
||||
{%- if un %}
|
||||
net use <b>w:</b> http{{ s }}://{{ ep }}/{{ rvp }}{% if accs %} <b>{{ pw }}</b> /user:{{ b_un }}{% endif %}
|
||||
{%- else %}
|
||||
net use <b>w:</b> http{{ s }}://{{ ep }}/{{ rvp }}{% if accs %} k /user:<b>{{ pw }}</b>{% endif %}
|
||||
{%- endif %}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div class="os lin">
|
||||
<p>rclone (v1.63 or later) is recommended:</p>
|
||||
<pre>
|
||||
rclone config create {{ aname }}-dav webdav url=http{{ s }}://{{ rip }}{{ hport }} vendor=owncloud pacer_min_sleep=0.01ms{% if accs %} user=k pass=<b>{{ pw }}</b>{% endif %}
|
||||
rclone config create {{ aname }}-dav webdav url=http{{ s }}://{{ rip }}{{ hport }} vendor=owncloud pacer_min_sleep=0.01ms{% if accs %} user={{ b_un }} pass=<b>{{ pw }}</b>{% endif %}
|
||||
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-dav:{{ rvp }} <b>mp</b>
|
||||
</pre>
|
||||
<ul>
|
||||
{% if s %}
|
||||
{%- if s %}
|
||||
<li>running <code>rclone mount</code> on LAN (or just dont have valid certificates)? add <code>--no-check-certificate</code></li>
|
||||
{% endif %}
|
||||
{%- endif %}
|
||||
<li>running <code>rclone mount</code> as root? add <code>--allow-other</code></li>
|
||||
<li>old version of rclone? replace all <code>=</code> with <code> </code> (space)</li>
|
||||
</ul>
|
||||
<p>alternatively use davfs2 (requires root, is slower, forgets lastmodified-timestamp on upload):</p>
|
||||
<pre>
|
||||
yum install davfs2
|
||||
{%- if un %}
|
||||
{% if accs %}printf '%s\n' {{ b_un }} <b>{{ pw }}</b> | {% endif %}mount -t davfs -ouid=1000 http{{ s }}://{{ ep }}/{{ rvp }} <b>mp</b>
|
||||
{%- else %}
|
||||
{% if accs %}printf '%s\n' <b>{{ pw }}</b> k | {% endif %}mount -t davfs -ouid=1000 http{{ s }}://{{ ep }}/{{ rvp }} <b>mp</b>
|
||||
{%- endif %}
|
||||
</pre>
|
||||
{%- if accs %}
|
||||
<p>make davfs2 automount on boot:</p>
|
||||
<pre>
|
||||
{%- if un %}
|
||||
printf '%s\n' "http{{ s }}://{{ ep }}/{{ rvp }} {{ b_un }} <b>{{ pw }}</b>" >> /etc/davfs2/secrets
|
||||
{%- else %}
|
||||
printf '%s\n' "http{{ s }}://{{ ep }}/{{ rvp }} <b>{{ pw }}</b> k" >> /etc/davfs2/secrets
|
||||
{%- endif %}
|
||||
printf '%s\n' "http{{ s }}://{{ ep }}/{{ rvp }} <b>mp</b> davfs rw,user,uid=1000,noauto 0 0" >> /etc/fstab
|
||||
</pre>
|
||||
{%- endif %}
|
||||
<p>or the emergency alternative (gnome/gui-only):</p>
|
||||
<!-- gnome-bug: ignores vp -->
|
||||
<pre>
|
||||
{%- if accs %}
|
||||
echo <b>{{ pw }}</b> | gio mount dav{{ s }}://k@{{ ep }}/{{ rvp }}
|
||||
echo <b>{{ pw }}</b> | gio mount dav{{ s }}://{{ b_un }}@{{ ep }}/{{ rvp }}
|
||||
{%- else %}
|
||||
gio mount -a dav{{ s }}://{{ ep }}/{{ rvp }}
|
||||
{%- endif %}
|
||||
|
@ -107,18 +121,18 @@
|
|||
|
||||
<div class="os mac">
|
||||
<pre>
|
||||
osascript -e ' mount volume "http{{ s }}://k:<b>{{ pw }}</b>@{{ ep }}/{{ rvp }}" '
|
||||
osascript -e ' mount volume "http{{ s }}://{{ b_un }}:<b>{{ pw }}</b>@{{ ep }}/{{ rvp }}" '
|
||||
</pre>
|
||||
<p>or you can open up a Finder, press command-K and paste this instead:</p>
|
||||
<pre>
|
||||
http{{ s }}://k:<b>{{ pw }}</b>@{{ ep }}/{{ rvp }}
|
||||
http{{ s }}://{{ b_un }}:<b>{{ pw }}</b>@{{ ep }}/{{ rvp }}
|
||||
</pre>
|
||||
|
||||
{% if s %}
|
||||
{%- if s %}
|
||||
<p><em>replace <code>https</code> with <code>http</code> if it doesn't work</em></p>
|
||||
{% endif %}
|
||||
{%- endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{%- endif %}
|
||||
|
||||
|
||||
|
||||
|
@ -127,51 +141,71 @@
|
|||
|
||||
<div class="os win">
|
||||
<p>if you can, install <a href="https://winfsp.dev/rel/">winfsp</a>+<a href="https://downloads.rclone.org/rclone-current-windows-amd64.zip">rclone</a> and then paste this in cmd:</p>
|
||||
{% if args.ftp %}
|
||||
{%- if args.ftp %}
|
||||
<p>connect with plaintext FTP:</p>
|
||||
<pre>
|
||||
{%- if un %}
|
||||
rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false
|
||||
{%- else %}
|
||||
rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false
|
||||
{%- endif %}
|
||||
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-ftp:{{ rvp }} <b>W:</b>
|
||||
</pre>
|
||||
{% endif %}
|
||||
{% if args.ftps %}
|
||||
{%- endif %}
|
||||
{%- if args.ftps %}
|
||||
<p>connect with TLS-encrypted FTPS:</p>
|
||||
<pre>
|
||||
{%- if un %}
|
||||
rclone config create {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false explicit_tls=true
|
||||
{%- else %}
|
||||
rclone config create {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false explicit_tls=true
|
||||
{%- endif %}
|
||||
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-ftps:{{ rvp }} <b>W:</b>
|
||||
</pre>
|
||||
{% endif %}
|
||||
{%- endif %}
|
||||
<ul>
|
||||
{% if args.ftps %}
|
||||
{%- if args.ftps %}
|
||||
<li>running on LAN (or just dont have valid certificates)? add <code>no_check_certificate=true</code> to the config command</li>
|
||||
{% endif %}
|
||||
{%- endif %}
|
||||
<li>old version of rclone? replace all <code>=</code> with <code> </code> (space)</li>
|
||||
</ul>
|
||||
<p>if you want to use the native FTP client in windows instead (please dont), press <code>win+R</code> and run this command:</p>
|
||||
<pre>
|
||||
{%- if un %}
|
||||
explorer {{ "ftp" if args.ftp else "ftps" }}://{% if accs %}{{ b_un }}:<b>{{ pw }}</b>@{% endif %}{{ host }}:{{ args.ftp or args.ftps }}/{{ rvp }}
|
||||
{%- else %}
|
||||
explorer {{ "ftp" if args.ftp else "ftps" }}://{% if accs %}<b>{{ pw }}</b>:k@{% endif %}{{ host }}:{{ args.ftp or args.ftps }}/{{ rvp }}
|
||||
{%- endif %}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div class="os lin">
|
||||
{% if args.ftp %}
|
||||
{%- if args.ftp %}
|
||||
<p>connect with plaintext FTP:</p>
|
||||
<pre>
|
||||
{%- if un %}
|
||||
rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false
|
||||
{%- else %}
|
||||
rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false
|
||||
{%- endif %}
|
||||
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-ftp:{{ rvp }} <b>mp</b>
|
||||
</pre>
|
||||
{% endif %}
|
||||
{% if args.ftps %}
|
||||
{%- endif %}
|
||||
{%- if args.ftps %}
|
||||
<p>connect with TLS-encrypted FTPS:</p>
|
||||
<pre>
|
||||
{%- if un %}
|
||||
rclone config create {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} user={% if accs %}{{ b_un }} pass=<b>{{ pw }}</b>{% else %}anonymous pass=k{% endif %} tls=false explicit_tls=true
|
||||
{%- else %}
|
||||
rclone config create {{ aname }}-ftps ftp host={{ rip }} port={{ args.ftps }} pass=k user={% if accs %}<b>{{ pw }}</b>{% else %}anonymous{% endif %} tls=false explicit_tls=true
|
||||
{%- endif %}
|
||||
rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-ftps:{{ rvp }} <b>mp</b>
|
||||
</pre>
|
||||
{% endif %}
|
||||
{%- endif %}
|
||||
<ul>
|
||||
{% if args.ftps %}
|
||||
{%- if args.ftps %}
|
||||
<li>running on LAN (or just dont have valid certificates)? add <code>no_check_certificate=true</code> to the config command</li>
|
||||
{% endif %}
|
||||
{%- endif %}
|
||||
<li>running <code>rclone mount</code> as root? add <code>--allow-other</code></li>
|
||||
<li>old version of rclone? replace all <code>=</code> with <code> </code> (space)</li>
|
||||
</ul>
|
||||
|
@ -179,7 +213,7 @@
|
|||
<!-- gnome-bug: ignores vp -->
|
||||
<pre>
|
||||
{%- if accs %}
|
||||
echo <b>{{ pw }}</b> | gio mount ftp{{ "" if args.ftp else "s" }}://k@{{ host }}:{{ args.ftp or args.ftps }}/{{ rvp }}
|
||||
echo <b>{{ pw }}</b> | gio mount ftp{{ "" if args.ftp else "s" }}://{{ b_un }}@{{ host }}:{{ args.ftp or args.ftps }}/{{ rvp }}
|
||||
{%- else %}
|
||||
gio mount -a ftp{{ "" if args.ftp else "s" }}://{{ host }}:{{ args.ftp or args.ftps }}/{{ rvp }}
|
||||
{%- endif %}
|
||||
|
@ -189,10 +223,10 @@
|
|||
<div class="os mac">
|
||||
<p>note: FTP is read-only on macos; please use WebDAV instead</p>
|
||||
<pre>
|
||||
open {{ "ftp" if args.ftp else "ftps" }}://{% if accs %}k:<b>{{ pw }}</b>@{% else %}anonymous:@{% endif %}{{ host }}:{{ args.ftp or args.ftps }}/{{ rvp }}
|
||||
open {{ "ftp" if args.ftp else "ftps" }}://{% if accs %}{{ b_un }}:<b>{{ pw }}</b>@{% else %}anonymous:@{% endif %}{{ host }}:{{ args.ftp or args.ftps }}/{{ rvp }}
|
||||
</pre>
|
||||
</div>
|
||||
{% endif %}
|
||||
{%- endif %}
|
||||
|
||||
|
||||
|
||||
|
@ -204,11 +238,11 @@
|
|||
<span class="os lin">doesn't need root</span>
|
||||
</p>
|
||||
<pre>
|
||||
partyfuse.py{% if accs %} -a <b>{{ pw }}</b>{% endif %} http{{ s }}://{{ ep }}/{{ rvp }} <b><span class="os win">W:</span><span class="os lin mac">mp</span></b>
|
||||
partyfuse.py{% if accs %} -a <b>{{ unpw }}</b>{% endif %} http{{ s }}://{{ ep }}/{{ rvp }} <b><span class="os win">W:</span><span class="os lin mac">mp</span></b>
|
||||
</pre>
|
||||
{% if s %}
|
||||
{%- if s %}
|
||||
<ul><li>if you are on LAN (or just dont have valid certificates), add <code>-td</code></li></ul>
|
||||
{% endif %}
|
||||
{%- endif %}
|
||||
<p>
|
||||
you can use <a href="{{ r }}/.cpr/a/u2c.py">u2c.py</a> to upload (sometimes faster than web-browsers)
|
||||
</p>
|
||||
|
@ -217,6 +251,10 @@
|
|||
{% if args.smb %}
|
||||
<h1>SMB / CIFS</h1>
|
||||
|
||||
{%- if un %}
|
||||
<h2>not available on this server because <code>--usernames</code> is enabled in the server config</h2>
|
||||
{%- else %}
|
||||
|
||||
<div class="os win">
|
||||
<pre>
|
||||
net use <b>w:</b> \\{{ host }}\a{% if accs %} k /user:<b>{{ pw }}</b>{% endif %}
|
||||
|
@ -234,7 +272,8 @@
|
|||
<pre class="os mac">
|
||||
open 'smb://<b>{{ pw }}</b>:k@{{ host }}/a'
|
||||
</pre>
|
||||
{% endif %}
|
||||
{%- endif %}
|
||||
{%- endif %}
|
||||
|
||||
|
||||
|
||||
|
@ -247,7 +286,7 @@
|
|||
{ "Version": "15.0.0", "Name": "copyparty",
|
||||
"RequestURL": "http{{ s }}://{{ ep }}/{{ rvp }}",
|
||||
"Headers": {
|
||||
{% if accs %}"pw": "<b>{{ pw }}</b>", {% endif %}"accept": "url"
|
||||
{% if accs %}"pw": "<b>{{ unpw }}</b>", {% endif %}"accept": "url"
|
||||
},
|
||||
"DestinationType": "ImageUploader, TextUploader, FileUploader",
|
||||
"Body": "MultipartFormData", "URL": "{response}",
|
||||
|
@ -260,7 +299,7 @@
|
|||
{ "Name": "copyparty",
|
||||
"RequestURL": "http{{ s }}://{{ ep }}/{{ rvp }}",
|
||||
"Headers": {
|
||||
{% if accs %}"pw": "<b>{{ pw }}</b>", {% endif %}"accept": "url"
|
||||
{% if accs %}"pw": "<b>{{ unpw }}</b>", {% endif %}"accept": "url"
|
||||
},
|
||||
"DestinationType": "ImageUploader, TextUploader, FileUploader",
|
||||
"FileFormName": "f" }
|
||||
|
@ -278,7 +317,9 @@
|
|||
{ "Name": "copyparty",
|
||||
"RequestURL": "http{{ s }}://{{ ep }}/{{ rvp }}",
|
||||
"Headers": {
|
||||
{% if accs %}"pw": "<b>{{ pw }}</b>",{% endif %}
|
||||
{%- if accs %}
|
||||
"pw": "<b>{{ unpw }}</b>",
|
||||
{%- endif %}
|
||||
"accept": "json"
|
||||
},
|
||||
"ResponseURL": "{{ '{{fileurl}}' }}",
|
||||
|
@ -295,7 +336,7 @@
|
|||
|
||||
<pre class="dl" name="flameshot.sh">
|
||||
#!/bin/bash
|
||||
pw="<b>{{ pw }}</b>"
|
||||
pw="<b>{{ unpw }}</b>"
|
||||
url="http{{ s }}://{{ ep }}/{{ rvp }}"
|
||||
filename="$(date +%Y-%m%d-%H%M%S).png"
|
||||
flameshot gui -s -r | curl -sT- "$url$filename?want=url&pw=$pw" | xsel -ib
|
||||
|
|
|
@ -49,21 +49,47 @@ function setos(os) {
|
|||
setos(WINDOWS ? 'win' : LINUX ? 'lin' : MACOS ? 'mac' : 'idk');
|
||||
|
||||
|
||||
var pw = '';
|
||||
var un, un0, pw, pw0, unpw, up0;
|
||||
function setpw(e) {
|
||||
ev(e);
|
||||
if (!ebi('un0'))
|
||||
return askpw();
|
||||
|
||||
modal.prompt('username:', '', function (v) {
|
||||
if (!v)
|
||||
return;
|
||||
|
||||
un = v;
|
||||
un0 = ebi('un0').innerHTML;
|
||||
var oa = QSA('b');
|
||||
|
||||
for (var a = 0; a < oa.length; a++)
|
||||
if (oa[a].innerHTML == un0)
|
||||
oa[a].textContent = un;
|
||||
|
||||
askpw();
|
||||
});
|
||||
}
|
||||
function askpw() {
|
||||
modal.prompt('password:', '', function (v) {
|
||||
if (!v)
|
||||
return;
|
||||
|
||||
pw = v;
|
||||
var pw0 = ebi('pw0').innerHTML,
|
||||
oa = QSA('b');
|
||||
|
||||
pw0 = ebi('pw0').innerHTML;
|
||||
var oa = QSA('b');
|
||||
|
||||
for (var a = 0; a < oa.length; a++)
|
||||
if (oa[a].innerHTML == pw0)
|
||||
oa[a].textContent = v;
|
||||
oa[a].textContent = pw;
|
||||
|
||||
if (un) {
|
||||
unpw = un ? (un+':'+pw) : pw;
|
||||
up0 = ebi('up0').innerHTML;
|
||||
for (var a = 0; a < oa.length; a++)
|
||||
if (oa[a].innerHTML == up0)
|
||||
oa[a].textContent = unpw;
|
||||
}
|
||||
add_dls();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -430,6 +430,15 @@ html.y textarea:focus {
|
|||
.mdo code {
|
||||
font-size: .96em;
|
||||
}
|
||||
html.z .mdo a>code,
|
||||
html.y .mdo a>code {
|
||||
color: inherit;
|
||||
background: inherit;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
border: none;
|
||||
}
|
||||
.mdo h1,
|
||||
.mdo h2 {
|
||||
line-height: 1.5em;
|
||||
|
|
|
@ -50,7 +50,7 @@ catch (ex) {
|
|||
}
|
||||
catch (ex) {
|
||||
console.log('up2k init failed:', ex);
|
||||
toast.err(10, 'could not initialze up2k\n\n' + basenames(ex));
|
||||
toast.err(10, 'could not initialize up2k\n\n' + basenames(ex));
|
||||
}
|
||||
}
|
||||
treectl.onscroll();
|
||||
|
@ -732,7 +732,7 @@ function Donut(uc, st) {
|
|||
tstrober = setInterval(strobe, 300);
|
||||
|
||||
if (uc.upsfx && actx && actx.state != 'suspended')
|
||||
sfx();
|
||||
sfx_nice();
|
||||
|
||||
// firefox may forget that filedrops are user-gestures so it can skip this:
|
||||
if (uc.upnag && Notification && Notification.permission == 'granted')
|
||||
|
@ -745,8 +745,10 @@ function Donut(uc, st) {
|
|||
if (!txt)
|
||||
clearInterval(tstrober);
|
||||
}
|
||||
}
|
||||
|
||||
function sfx() {
|
||||
function sfx_nice() {
|
||||
if (true) {
|
||||
var osc = actx.createOscillator(),
|
||||
gain = actx.createGain(),
|
||||
gg = gain.gain,
|
||||
|
@ -1801,7 +1803,7 @@ function up2k_init(subtle) {
|
|||
while (true) {
|
||||
var now = Date.now(),
|
||||
blocktime = now - r.tact,
|
||||
was_busy = st.is_busy,
|
||||
was_busy = !!st.is_busy,
|
||||
is_busy = !!( // gzip take the wheel
|
||||
st.car < st.files.length ||
|
||||
st.busy.hash.length ||
|
||||
|
@ -3037,10 +3039,12 @@ function up2k_init(subtle) {
|
|||
if (anymod(e))
|
||||
return;
|
||||
|
||||
if (e.code == 'ArrowUp')
|
||||
var k = e.key || e.code;
|
||||
|
||||
if (k == 'ArrowUp')
|
||||
bumpthread(1);
|
||||
|
||||
if (e.code == 'ArrowDown')
|
||||
if (k == 'ArrowDown')
|
||||
bumpthread(-1);
|
||||
}
|
||||
|
||||
|
@ -3103,7 +3107,8 @@ function up2k_init(subtle) {
|
|||
ebi('u2szg').addEventListener('blur', read_u2sz);
|
||||
ebi('u2szg').onkeydown = function (e) {
|
||||
if (anymod(e)) return;
|
||||
var n = e.code == 'ArrowUp' ? 1 : e.code == 'ArrowDown' ? -1 : 0;
|
||||
var k = e.key || e.code,
|
||||
n = k == 'ArrowUp' ? 1 : k == 'ArrowDown' ? -1 : 0;
|
||||
if (!n) return;
|
||||
this.value = parseInt(this.value) + n;
|
||||
read_u2sz();
|
||||
|
@ -3180,7 +3185,8 @@ function up2k_init(subtle) {
|
|||
|
||||
function kd_life(e) {
|
||||
var el = e.target,
|
||||
d = e.code == 'ArrowUp' ? 1 : e.code == 'ArrowDown' ? -1 : 0;
|
||||
k = e.key || e.code,
|
||||
d = k == 'ArrowUp' ? 1 : k == 'ArrowDown' ? -1 : 0;
|
||||
|
||||
if (anymod(e) || !d)
|
||||
return;
|
||||
|
@ -3421,6 +3427,8 @@ if (QS('#op_up2k.act'))
|
|||
goto_up2k();
|
||||
|
||||
apply_perms({ "perms": perms, "frand": frand, "u2ts": u2ts });
|
||||
if (ls0)
|
||||
fileman.render();
|
||||
|
||||
|
||||
(function () {
|
||||
|
|
|
@ -960,15 +960,95 @@ function f2f(val, nd) {
|
|||
}
|
||||
|
||||
|
||||
var HSZ_U = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
||||
function humansize(b, terse) {
|
||||
var i = 0, u = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
||||
while (b >= 1000 && i < u.length - 1) {
|
||||
b /= 1024;
|
||||
i += 1;
|
||||
}
|
||||
var i = 0;
|
||||
while (b >= 1000 && i < 5) { b /= 1024; i += 1; }
|
||||
return (f2f(b, b >= 100 ? 0 : b >= 10 ? 1 : 2) +
|
||||
' ' + (terse ? u[i].charAt(0) : u[i]));
|
||||
' ' + (terse ? HSZ_U[i].charAt(0) : HSZ_U[i]));
|
||||
}
|
||||
function humansize_su(b) {
|
||||
var i = 0;
|
||||
while (b >= 1000 && i < 5) { b /= 1024; i += 1; }
|
||||
return [b, HSZ_U[i]];
|
||||
}
|
||||
function humansize_0(b) {
|
||||
return '' + b;
|
||||
}
|
||||
function humansize_1(b) {
|
||||
return ('' + b).replace(/\B(?=(\d{3})+(?!\d))/g, " ");
|
||||
}
|
||||
function humansize_2g(b) {
|
||||
var z = humansize_su(b), u = z[1].charAt(0); b = z[0];
|
||||
return [f2f(b, b >= 100 ? 0 : b >= 10 ? 1 : 2) + ' ' + u, u];
|
||||
}
|
||||
function humansize_3g(b) {
|
||||
var z = humansize_su(b), u = z[1].charAt(0); b = z[0];
|
||||
return [f2f(b, b >= 10 ? 0 : 1) + ' ' + u, u];
|
||||
}
|
||||
function humansize_4g(b) {
|
||||
var z = humansize_su(b), u = z[1]; b = z[0];
|
||||
return [parseFloat(b.toFixed(b >= 100 ? 0 : b >= 10 ? 1 : 2)) + ' ' + u, u.charAt(0)];
|
||||
}
|
||||
function humansize_5g(b) {
|
||||
var z = humansize_su(b), u = z[1]; b = z[0];
|
||||
return [parseFloat(b.toFixed(b >= 10 ? 0 : 1)) + ' ' + u, u.charAt(0)];
|
||||
}
|
||||
function humansize_2(b) {
|
||||
return humansize_2g(b)[0];
|
||||
}
|
||||
function humansize_3(b) {
|
||||
return humansize_3g(b)[0];
|
||||
}
|
||||
function humansize_4(b) {
|
||||
return humansize_4g(b)[0];
|
||||
}
|
||||
function humansize_5(b) {
|
||||
return humansize_5g(b)[0];
|
||||
}
|
||||
function humansize_2c(b) {
|
||||
var v = humansize_2g(b);
|
||||
return '<span class="fsz_' + v[1].charAt(0) + '">' + v[0] + '</span>';
|
||||
}
|
||||
function humansize_3c(b) {
|
||||
var v = humansize_3g(b);
|
||||
return '<span class="fsz_' + v[1].charAt(0) + '">' + v[0] + '</span>';
|
||||
}
|
||||
function humansize_4c(b) {
|
||||
var v = humansize_4g(b);
|
||||
return '<span class="fsz_' + v[1].charAt(0) + '">' + v[0] + '</span>';
|
||||
}
|
||||
function humansize_5c(b) {
|
||||
var v = humansize_5g(b);
|
||||
return '<span class="fsz_' + v[1].charAt(0) + '">' + v[0] + '</span>';
|
||||
}
|
||||
function humansize_fuzzy(b) {
|
||||
if (b <= 0) return "yes";
|
||||
if (b <= 80) return "hullkort";
|
||||
if (b <= 368640) return "5¼ DD";
|
||||
if (b <= 1474560) return "save icon";
|
||||
if (b <= 2880000) return "3½ Extended";
|
||||
if (b <= 13107200) return "C90 Tape";
|
||||
if (b <= 21000000) return "Floptical";
|
||||
if (b <= 33554432) return "MPMan F10";
|
||||
if (b <= 50000000) return "creditcardCD";
|
||||
if (b <= 100663296) return "Zipdisk";
|
||||
if (b <= 170000000) return "MD";
|
||||
if (b <= 220200960) return "8cm CD";
|
||||
if (b <= 737280000) return "CD-R";
|
||||
if (b <= 900000000) return "UMD";
|
||||
if (b <= 1300000000) return "GD-ROM";
|
||||
if (b <= 4700000000) return "DVD";
|
||||
if (b <= 9400000000) return "DVD-DL";
|
||||
if (b <= 25025000000) return "BluRei";
|
||||
if (b <= 50050000000) return "BD-DL";
|
||||
return "LTO";
|
||||
}
|
||||
var humansize_fmts = ['0', '1', '2', '2c', '3', '3c', '4', '4c', '5', '5c', 'fuzzy'];
|
||||
window.filesizefun = (function () {
|
||||
var v = sread('fszfmt', humansize_fmts);
|
||||
return window['humansize_' + (v || window.dfszf)] || humansize_1;
|
||||
})();
|
||||
|
||||
|
||||
function humantime(v) {
|
||||
|
@ -1824,12 +1904,12 @@ var modal = (function () {
|
|||
};
|
||||
|
||||
var onkey = function (e) {
|
||||
var k = (e.code || e.key) + '',
|
||||
var k = (e.key || e.code) + '',
|
||||
eok = ebi('modal-ok'),
|
||||
eng = ebi('modal-ng'),
|
||||
ae = document.activeElement;
|
||||
|
||||
if (k == 'Space' && ae && (ae === eok || ae === eng))
|
||||
if ((k == 'Space' || k == 'Spacebar' || k == ' ') && ae && (ae === eok || ae === eng))
|
||||
k = 'Enter';
|
||||
|
||||
if (k.endsWith('Enter')) {
|
||||
|
@ -2181,6 +2261,19 @@ function bchrome() {
|
|||
}
|
||||
bchrome();
|
||||
|
||||
var XC_CMSG = {
|
||||
502: "bad gateway (server offline)",
|
||||
503: "server offline",
|
||||
504: "gateway timeout (server busy)",
|
||||
529: "gateway timeout (server busy)",
|
||||
520: "unknown error from server",
|
||||
521: "server offline",
|
||||
523: "server offline",
|
||||
522: "proxy timeout (server busy)",
|
||||
524: "proxy timeout (server busy)",
|
||||
598: "proxy timeout (server busy)",
|
||||
599: "proxy timeout (server busy)",
|
||||
};
|
||||
var cf_cha_t = 0;
|
||||
function xhrchk(xhr, prefix, e404, lvl, tag) {
|
||||
if (xhr.status < 400 && xhr.status >= 200)
|
||||
|
@ -2223,5 +2316,8 @@ function xhrchk(xhr, prefix, e404, lvl, tag) {
|
|||
document.body.appendChild(fr);
|
||||
}
|
||||
|
||||
if (XC_CMSG[xhr.status] && (errtxt.indexOf('<html') + 1))
|
||||
errtxt = XC_CMSG[xhr.status];
|
||||
|
||||
return fun(0, prefix + xhr.status + ": " + errtxt, tag);
|
||||
}
|
||||
|
|
|
@ -1,3 +1,363 @@
|
|||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2025-0929-2310 `v1.19.15` merry christmas
|
||||
|
||||
## 🧪 new features
|
||||
|
||||
* #184 add various human-readable formats for filesizes 234eddec
|
||||
* search for files by their identifier ("wark"/checksum) 4e38e408
|
||||
* and those are displayed in file-listings now too 456addf2
|
||||
* PUT-upload with header `Replace` will overwrite any existing files 397ed565
|
||||
* xbu/xau hooks can reject uploads with a custom message df0fa9d1
|
||||
* #855 mDNS options to change the announced http/https port a3d95067
|
||||
* #473 #383 custom favicons per-volume (.ico/png/gif/svg) 470b5048
|
||||
* doesn't seem to work in internet explorer... ah whatever, go next
|
||||
|
||||
## 🩹 bugfixes
|
||||
|
||||
* #849 create IdP-db for `--idp-store` when necessary 80ca7851
|
||||
* #859 cbz-thumbnailing had an accidental dependency on FFmpeg 983865d9
|
||||
* docs: misleading markdown-expansion example e187df28
|
||||
|
||||
## 🔧 other changes
|
||||
|
||||
* #851 show a huge warning when copyparty accidentally detects a failing HDD and/or filesystem-corruption during indexing 6912e867 eb5d767b
|
||||
* #870 improved discord video embeds (thx @tsuza!) f0ecb083
|
||||
* #858 prefer reflinks (not hardlinks) in the `-ss` security option 57650a21
|
||||
* improved controlpanel action-buttons layout 9f46e4db
|
||||
|
||||
## 🌠 fun facts
|
||||
|
||||
* includes (a tiny bit of) code written at [koie ramen](https://a.ocv.me/pub/g/2025/09/PXL_20250925_151716836.jpg)
|
||||
* [according to Biltema](https://a.ocv.me/pub/g/2025/09/PXL_20250927_160446367~2.jpg), september is an excellent time to start decorating for xmas
|
||||
|
||||
<img src="https://a.ocv.me/pub/stuff/padoru.gif" alt="padoru" /> <img src="https://a.ocv.me/pub/stuff/padoru.gif" alt="padoru" /> <img src="https://a.ocv.me/pub/stuff/padoru.gif" alt="padoru" />
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2025-0923-2247 `v1.19.14` Voile, the Magic Library
|
||||
|
||||
## 🧪 new features
|
||||
|
||||
* #779 add [OPDS](https://opds.io/) support (thx @Scotsguy!) 6dbd9901
|
||||
* copyparty can now serve books for [KOReader](https://koreader.rocks/)
|
||||
* [the mandatory soundtrack](https://www.youtube.com/watch?v=F8Aex6tzH-s)
|
||||
* #786 add Turkish translation (thx @NandeMD!) 549fe33f
|
||||
* #808 support reading config-files in UTF8-BOM 5e4ff90b
|
||||
* make more http-errors return a friendly errortext rather than the scary wall of html 9d066414
|
||||
|
||||
## 🩹 bugfixes
|
||||
|
||||
* #842 could not navpane into webroot if webroot is unmapped 0941fd4e
|
||||
* upload-resume becomes funky when the OS/network is overloaded to the point where it starts dropping connections left and right -- the issue was reported on discord and I don't have a good way to reproduce it, but these changes may help and/or fix it:
|
||||
* b136a5b0 panic and drop chunk reservations if client or connection glitches out
|
||||
* 38df223b also drop reservations if subchunk logic hits an edgecase
|
||||
|
||||
## 🔧 other changes
|
||||
|
||||
* [versus.md](https://github.com/9001/copyparty/blob/hovudstraum/docs/versus.md) tweaks:
|
||||
* #840 tooltips in the table headers (thx @guano!) e9ca36fa
|
||||
* #839 sftpgo updates (thx @augustanational!) a053a663
|
||||
|
||||
## 🌠 fun facts
|
||||
|
||||
* this release is identical to v1.19.13 except [the pypi package isn't messed up](https://github.com/9001/copyparty/issues/847) 👉😎👉
|
||||
* as if the 13 wasn't foreshadowing enough
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2025-0921-2211 `v1.19.12` conlangparty
|
||||
|
||||
## 🧪 new features
|
||||
|
||||
* #787 add Esperanto translation (thx @slashdevslashurandom!) 15d3c2fb
|
||||
* #802 timezone can be specified for the rotf upload rule (thx @Lehmustus!) 1460fe97
|
||||
|
||||
## 🩹 bugfixes
|
||||
|
||||
* #837 sharing an entire HDD on Windows ([v1.19.9](https://github.com/9001/copyparty/releases/tag/v1.19.9) regression) 6a244320
|
||||
* sharing your whole [【Dドライブ】](https://www.youtube.com/watch?v=BFfYrxm2t58) is once again possible
|
||||
* TLNote: `Dドライブ` means "D:\ drive"
|
||||
* if you can't upgrade, a workaround is global-option `casechk: n`
|
||||
* `/?ls` on an unmapped root didn't give a sensible response; now it should be okay except it won't have a `cfg` field 8f6194fe
|
||||
|
||||
## 🔧 other changes
|
||||
|
||||
* #836 hide the unpost tab in folders where user has no write-access ca872c40
|
||||
* #835 fix webdep buildscript to avoid an edgecase on some platforms (thx @25huizengek1!) 260da2f4
|
||||
|
||||
## 🌠 fun facts
|
||||
|
||||
* the esperanto translation was the final straw; `copyparty-sfx.py` is now 1 MiB large
|
||||
* `copyparty-en.py` is still a comfy 759 KiB
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2025-0920-1011 `v1.19.11` ftp fix
|
||||
|
||||
## 🩹 bugfixes
|
||||
|
||||
* #827 ftp on servers with unmapped root broke in v1.19.9 280815f1
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2025-0919-2244 `v1.19.10` ramdisk kinshi
|
||||
|
||||
## 🧪 new features
|
||||
|
||||
* prevent uploading into ramdisks by default 59a01221 538a205c
|
||||
* safeguard against misconfigured docker containers, where certain parts of the vfs has not been mapped to actual storage, for example `/w/music` is but `/w/` itself isn't
|
||||
* can be disabled with `wram` (global-option and/or volflag), mainly for ephemeral servers
|
||||
* #799 nixos: groups can be specified (thx @AnyTimeTraveler!) ee5f3190
|
||||
* the logspam from the filesystem indexer can be reduced/disabled 478f1c76
|
||||
* new options `scan-st-r`, `scan-pr-r`, `scan-pr-s`
|
||||
|
||||
## 🩹 bugfixes
|
||||
|
||||
* #809 medialinks (`#af-badf00d`) would fail on the very first pageload from a new browser 5996a58b
|
||||
* #806 instructions for running on iOS was bad (thx @GhelloZ!) 35326a6f
|
||||
|
||||
## 🔧 other changes
|
||||
|
||||
* copyparty32.exe is now english-only, to save space 669b1075
|
||||
* version info on startup indicates free-threading or not 65591528
|
||||
* docs: explain the `daw` option better a043d7cf
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2025-0915-0019 `v1.19.9` case-sensitivity, give or take
|
||||
|
||||
## 🧪 new features
|
||||
|
||||
* #781 case-sensitive behavior is now simulated on Windows/Macos/Fat32/NTFS 8b66874b
|
||||
* avoids some of the scary issues associated with case-insensitive filesystems
|
||||
* unfortunately this is expensive and may be **noticeably slower in large folders;** disable the safeguard with `casechk: n` if you know you don't need it
|
||||
* #789 case-insensitive search for unicode filenames/paths (thx @km-clay!) e2aa8fc1 ecd18adc
|
||||
* default-disabled because it is somewhat expensive; enable with global-option `srch-icase`
|
||||
* [CB-1](https://codeberg.org/9001/copyparty/issues/1) add `--qr-stdout` and `--qr-stderr` to show qr-code even with `-q` d7887f3d
|
||||
|
||||
## 🩹 bugfixes
|
||||
|
||||
* #775 the basic-uploader didn't accept empty files 25749b4b
|
||||
* opt-out from index.html with `?v` did not work as documented 3d09bec1
|
||||
* Windows: dedup could get rejected by the filesystem if the origin file had a timestamp from the cambrian era e09f3c9e
|
||||
* webdav would incorrectly return an error for Depth:0 on an unmapped root 3a2381ff
|
||||
* markdown-editor would waste another http roundtrip on certain documents 14b7e514
|
||||
* `--help` didn't render if terminal was non-UTF8 3f454927
|
||||
|
||||
## 🔧 other changes
|
||||
|
||||
* #788 fixed a hotkey typo in the imageviewer (thx @tkroo!) 5c1a43c7
|
||||
* #778 improved polish translation (thx @daimond113!) 52438bcc
|
||||
* #798 debian: fixed an issue in the systemd script (thx @Beethoven-n, and congrats on commit number 4000!) dfd9e007
|
||||
* media-tag `conductor` is no longer mapped to `circle` (album-artist) 9c9e4057
|
||||
* "download-selection-as-zip" now produces a better filename, `sel-FOLDERNAME.zip` instead of `FIRSTFILE.zip` 8f587627
|
||||
* detect and warn if IdP volumes are misconfigured in a particular way 83bd1974
|
||||
|
||||
## 🌠 fun facts
|
||||
|
||||
* the themesong of this release is [KO3 - Give it up?](https://www.youtube.com/watch?v=8w_na7HAppU) because that's what the car mechanic got to enjoy when i forgot to unplug the flashdrive before handing in the shitbox for service
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2025-0907-2300 `v1.19.8` SECURITY: fix single-file shares
|
||||
|
||||
## ⚠️ ATTN: this release fixes [CVE-2025-58753](https://github.com/9001/copyparty/security/advisories/GHSA-pxvw-4w88-6x95), an issue with shares
|
||||
|
||||
* when a share is created for just one or more files inside a folder, it was possible to access the other files inside that folder by guessing the filenames
|
||||
* it was not possible to descend into subdirectories in this manner; only the sibling files were accessible
|
||||
* NOTE: this does NOT affect filekeys; this is specifically regarding the `shr` global-option
|
||||
|
||||
## recent important news
|
||||
|
||||
* [v1.19.8 (2025-09-07)](https://github.com/9001/copyparty/releases/tag/v1.19.8) fixed [CVE-2025-58753](https://github.com/9001/copyparty/security/advisories/GHSA-pxvw-4w88-6x95) (a missing permission-check inside single-file shares)
|
||||
* [v1.15.0 (2024-09-08)](https://github.com/9001/copyparty/releases/tag/v1.15.0) changed upload deduplication to be default-disabled
|
||||
* [v1.14.3 (2024-08-30)](https://github.com/9001/copyparty/releases/tag/v1.14.3) fixed a bug that was introduced in v1.13.8 (2024-08-13); this bug could lead to **data loss** -- see the v1.14.3 release-notes for details
|
||||
|
||||
## 🧪 new features
|
||||
|
||||
* #761 IdP: option to replace the login/logout links and buttons with redirects into an IdP UI 09f22993
|
||||
* #726 disk-usage and server-version can be selectively hidden according to user permissions 19a4c453
|
||||
* option `--shr-who` / volflag `shr_who` decides who is able to create a share of that volume edafa158
|
||||
* #751 nixos: add globalExtraConfig to specify repeatable config parameters (thx @xvrqt!) 09e3018b
|
||||
* some very small speedups (mainly u2c and ancient python versions) 74821a38
|
||||
* #759 #393 total folder size now decreases when files inside are deleted 96b109b0
|
||||
* would previously require a reindex to get back on track
|
||||
|
||||
## 🩹 bugfixes
|
||||
|
||||
* fix [GHSA-pxvw-4w88-6x95](https://github.com/9001/copyparty/security/advisories/GHSA-pxvw-4w88-6x95) by fencing fileshares to just the shared files e0a92ba7
|
||||
* #397 prevent hinting at valid passwords, even if they cannot be used to authenticate with 7a4ee4db
|
||||
* #747 disable some features if `/tmp` must be used for runtime config e6755aa8
|
||||
* the config-folder will now also be created with chmod 700 (accessible by owner only)
|
||||
* #733 #298 fix hotkeys on non-qwerty keyboard layouts (dvorak etc.) e798a9a5
|
||||
* #539 ftp-server: support clients which never does a CWD b0496311
|
||||
* ignore the plaintext session-cookie on https; fixes some confusing behavior when switching from https to http c71128fd
|
||||
* `og-ua` would prevent clients matching the pattern from accessing fullsize files
|
||||
* `og-ua` was only possible to set globally; the `og_ua` volflag was ignored 422f8f62
|
||||
* uds / unix-domain-sockets got wrong permissions when `rm-sck` was used e270fe60
|
||||
* #727 macos: support running from config-files 230a1462
|
||||
* #539 avoid issues if someone uploads a file with a last-modified timestamp from year -9999999999999 eeb7738b
|
||||
* using the spacebar to pause a video was jank on chrome bfcb6eac
|
||||
* block the next-song hotkey while a folder is loading f7e08ed0
|
||||
* #748 fix rare js-panic when an action is aborted aaeec11f
|
||||
* #738 bubbleparty: use /bin/bash (thx @ckastner!) 0469b5a2
|
||||
|
||||
## 🔧 other changes
|
||||
|
||||
* partyfuse: nice speedup by caching `readdir` too 06d2654b
|
||||
* partyfuse: explain usage with usernames 1cdb3880
|
||||
* connect-page: better examples when usernames enabled 3bdef75e
|
||||
* docker: fix image annotations ab562382
|
||||
|
||||
## 🌠 fun facts
|
||||
|
||||
* konami's biggest legacy lives on f0caf881 bd6d1f96
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2025-0828-2014 `v1.19.7` chdir
|
||||
|
||||
## 🧪 new features
|
||||
|
||||
* new option `chdir` to change the PWD (process working-directory) before volumes are mapped 14555d58
|
||||
|
||||
## 🩹 bugfixes
|
||||
|
||||
* fix using empty folders as statefile storage ([v1.19.6](https://github.com/9001/copyparty/releases/tag/v1.19.6) made this a bit too strict) 0d96786e
|
||||
* holding I/K to scroll through folders quickly now works better 914686ec
|
||||
|
||||
## 🔧 other changes
|
||||
|
||||
* #717 docker: fix the image repo metadata (thx @EmilyxFox!) 6f087117
|
||||
* docker: change `$HOME` to `/state` 01cf20a0 d1f75229
|
||||
* and use the new `chdir` option to preserve old config-file semantics 14555d58
|
||||
* helps avoid statefiles accidentally landing in `/w` as a consequence of misconfiguration
|
||||
|
||||
## 🌠 fun facts
|
||||
|
||||
* this release was made at [RevSpace NL](https://a.ocv.me/pub/g/nerd-stuff/PXL_20250828_202820075.jpg?cache)
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2025-0827-2038 `v1.19.6` auth-precedence
|
||||
|
||||
## 🧪 new features
|
||||
|
||||
* #673 add Portuguese translation (thx anonymous!) 4b8c2215
|
||||
* ...and enable the Polish translation (whoops) 8f235be6
|
||||
* #689 add option to control authentication priority/precedence 543b7ea9
|
||||
* url-parameter `?dl` forces file download instead of displaying in-browser 48d6224e
|
||||
* #533 more ways to make the QR-code always-visible in the console 2848941e
|
||||
* #695 option to log invalid xml from clients 28b93d79
|
||||
* #552 configurable markdown newline behavior 0491123b
|
||||
* and tweak the styling of monospace in links 68503444
|
||||
|
||||
## 🩹 bugfixes
|
||||
|
||||
* #628 FTP-server now accepts connections from IPv6 link-local addresses 978801d0
|
||||
* incorrect assumption that all IPv6 link-local addresses start with `fe80` d39c74c1
|
||||
* ftp: fix file rename d40f061a
|
||||
* u2c: couldn't upload files located at the very top of the unix file hierarchy 599e82f2
|
||||
* #699 markdown-editor: fix panic if the table-formatter is executed on something that isn't a table 4c042b3c
|
||||
|
||||
## 🔧 other changes
|
||||
|
||||
* #696 a volume can be one single file, not just folders aa1c9213
|
||||
* #442 strongly prefer XDG_CONFIG_HOME as config location 35472557
|
||||
* #691 album-art collected from audio-files can now become folder thumbnails 0b50fde3
|
||||
* allow spaces in more of the comma-separated options d30240b4
|
||||
* docs:
|
||||
* mention config requirements for [syncing folders](https://github.com/9001/copyparty/#folder-sync) with u2c 6cd0a396 59f142cd
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2025-0821-2319 `v1.19.5` it runs on iOS
|
||||
|
||||
## 🧪 new features
|
||||
|
||||
* #328 run copyparty on iPhones; see [install on iOS](https://github.com/9001/copyparty#install-on-iOS) in the readme ca98d54f
|
||||
* cannot run in the background, doesn't have full access to your files, and is slightly buggy, but it *works*
|
||||
* [running on android](https://github.com/9001/copyparty#install-on-android) gives you a much better experience
|
||||
* save the qr-code to a file (txt/svg/png) 202ddeac
|
||||
|
||||
## 🩹 bugfixes
|
||||
|
||||
* #661 fix incorrect `rproxy` hint in the logs 6c76614e
|
||||
* #649 fix js-crash when tapping in the exactly correct place (thx @hahaslav for debugging!) 0de07d8e
|
||||
* #628 ftpd: fix banning IPv6 clients 6d76254c
|
||||
|
||||
## 🔧 other changes
|
||||
|
||||
* #296 nixos: support non-flake setups (thx @Sorixelle!) 20ef74cd 32593670
|
||||
* config-parser catches and explains a few more common mistakes cc65b1b5
|
||||
* docs:
|
||||
* #490, #199: readme: confirm that combining copyparty and syncthing is safe c51371c7
|
||||
* #377 improved authelia docker example (thx @xFuture603!) cd8771fa
|
||||
* mention the homebrew formulae f9cb2c15
|
||||
* #651 versus.md: fix hfs3 comparison (thx @rejetto!) 7a4973fa
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2025-0817-1556 `v1.19.4` take two (fix cfg vols)
|
||||
|
||||
## ℹ️ this upgrade is a one-way ticket
|
||||
|
||||
* your up2k database (`.hist/up2k.db`), used by the `e2d` filesystem indexing feature, **will be upgraded to a new format** which older copyparty versions cannot read. A backup of each database will be created automatically, named `up2k.db.bak.SOMETHING.v5`. If you need to downgrade to a previous version: Shutdown copyparty, delete these files: `up2k.db up2k.db-shm up2k.db-wal` and then copy `up2k.db.bak.*.v5` to `up2k.db`
|
||||
|
||||
## 🧪 new features
|
||||
|
||||
* new translations:
|
||||
* #551 Swedish (thx @Bevinsky!) d676a86f
|
||||
* #551 Korean (thx @nyqui!) 4e878d2f
|
||||
* #581 new theme: phi95 (thx @varphi-online!) d8662aeb
|
||||
* #567 .raw image thumbnails (thx @ar-nelson!) 0177a9b4
|
||||
* available in docker-images `iv` and `dj`
|
||||
* #561 epub thumbnails (thx @Scotsguy!) 9435e6b2
|
||||
* #252 music thumbnails use embdded coverart if available 98d117b8
|
||||
* thumbnails folder `.hist/th` must be deleted to take effect
|
||||
* #530 show username of uploaders in file listings; requires `a` (admin) permission 4df033ec
|
||||
* #604 a new group `@acct` which automatically contains all known usernames 68907eaf
|
||||
* controlpanel has a dedicated "logout all sessions" button, similar to the logout-link in the browser f4a3fba2
|
||||
* #397 accounts can be restricted to certian IPs 62e072a2
|
||||
* #504 automatic login through tailscale auth a4649d1e
|
||||
* #533 sticky qr-code with `--qr-pin 1` 1ebe06f5
|
||||
* #572 button to abort copy/move 715d374e
|
||||
* #618 "download selected files" didn't work on firefox 52 (winxp) dcc6b1b4
|
||||
* max number of cookies to allow can be configured 6303effe
|
||||
* good if you have too many selfhosted services on one domain (but will beware of the spec-mandataed max length of the cookie field!)
|
||||
|
||||
## 🩹 bugfixes
|
||||
|
||||
* fix xvol/xdev edgecases:
|
||||
* #603 rootless vfs 554cc2f3
|
||||
* false-positive with overlapping volumes d9046f7e
|
||||
* #573 ftp: attempting an upload into read-only folder no longer kills the connection 3aa8b7aa
|
||||
* #306 adjust navpane for `--rp-loc` (location-based proxying)
|
||||
* #556 more sensible config expansion order f4727f8e
|
||||
* #624 ...which broke things bf1fdcab
|
||||
* the video player now stays fullscreen between videos 782e2f1d
|
||||
* heif thumbnailing with libvips
|
||||
|
||||
## 🔧 other changes
|
||||
|
||||
* #253 build nix-packages from source (thx @toast003, @chinponya!) 187cae25
|
||||
* #616 logfiles will have a plaintext severity column if `--no-ansi` d4cf42e7
|
||||
* #598 separate option `--ac-convt` for audio transcoding timeout d5623057
|
||||
* #596 users with a blank password gets a strong random-generated one 7f448750
|
||||
* copyparty.exe: upgrade to python 3.13.7
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2025-0810-1226 `v1.19.1` archlinux fix
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
* [general](#general)
|
||||
* [event hooks](#event-hooks) - on writing your own [hooks](../README.md#event-hooks)
|
||||
* [hook effects](#hook-effects) - hooks can cause intentional side-effects
|
||||
* [hook import](#hook-import) - the `I` flag runs the hook inside copyparty
|
||||
* [assumptions](#assumptions)
|
||||
* [mdns](#mdns)
|
||||
* [sfx repack](#sfx-repack) - reduce the size of an sfx by removing features
|
||||
|
@ -160,10 +161,12 @@ authenticate using header `Cookie: cppwd=foo` or url param `&pw=foo`
|
|||
|
||||
| method | params | result |
|
||||
|--|--|--|
|
||||
| GET | `?dl` | download file (don't show in-browser) |
|
||||
| GET | `?ls` | list files/folders at URL as JSON |
|
||||
| GET | `?ls&dots` | list files/folders at URL as JSON, including dotfiles |
|
||||
| GET | `?ls=t` | list files/folders at URL as plaintext |
|
||||
| GET | `?ls=v` | list files/folders at URL, terminal-formatted |
|
||||
| GET | `?opds` | list files/folders at URL as opds feed, for e-readers |
|
||||
| GET | `?lt` | in listings, use symlink timestamps rather than targets |
|
||||
| GET | `?b` | list files/folders at URL as simplified HTML |
|
||||
| GET | `?tree=.` | list one level of subdirectories inside URL |
|
||||
|
@ -244,6 +247,7 @@ upload modifiers:
|
|||
| `Accept: json` | `want=json` | return upload info as json; same as `?j` |
|
||||
| `Rand: 4` | `rand=4` | generate random filename with 4 characters |
|
||||
| `Life: 30` | `life=30` | delete file after 30 seconds |
|
||||
| `Replace: 1` | `replace` | overwrite file if exists |
|
||||
| `CK: no` | `ck` | disable serverside checksum (maybe faster) |
|
||||
| `CK: md5` | `ck=md5` | return md5 checksum instead of sha512 |
|
||||
| `CK: sha1` | `ck=sha1` | return sha1 checksum |
|
||||
|
@ -252,7 +256,9 @@ upload modifiers:
|
|||
| `CK: b2s` | `ck=b2s` | return blake2s checksum |
|
||||
|
||||
* `life` only has an effect if the volume has a lifetime, and the volume lifetime must be greater than the file's
|
||||
|
||||
* `replace` upload-modifier:
|
||||
* the header `replace: 1` works for both PUT and multipart-post
|
||||
* the url-param `replace` only works for multipart-post
|
||||
* server behavior of `msg` can be reconfigured with `--urlform`
|
||||
|
||||
## admin
|
||||
|
@ -305,6 +311,14 @@ a subset of effect types are available for a subset of hook types,
|
|||
to trigger indexing of files `/foo/1.txt` and `/foo/bar/2.txt`, a hook can `print(json.dumps({"idx":{"vp":["/foo/1.txt","/foo/bar/2.txt"]}}))` (and replace "idx" with "del" to delete instead)
|
||||
* note: paths starting with `/` are absolute URLs, but you can also do `../3.txt` relative to the destination folder of each uploaded file
|
||||
|
||||
## hook import
|
||||
|
||||
the `I` flag runs the hook inside copyparty, which can be very useful and dangerous:
|
||||
|
||||
* around 140x faster because it doesn't need to launch a new subprocess
|
||||
* the hook can intentionally (or accidentally) mess with copyparty's internals
|
||||
* very easy to crash things if not careful
|
||||
|
||||
|
||||
# assumptions
|
||||
|
||||
|
@ -328,7 +342,7 @@ if you don't need all the features, you can repack the sfx and save a bunch of s
|
|||
|
||||
the features you can opt to drop are
|
||||
* `cm`/easymde, the "fancy" markdown editor, saves ~89k
|
||||
* `hl`, prism, the syntax hilighter, saves ~41k
|
||||
* `hl`, prism, the syntax highlighter, saves ~41k
|
||||
* `fnt`, source-code-pro, the monospace font, saves ~9k
|
||||
|
||||
for the `re`pack to work, first run one of the sfx'es once to unpack it
|
||||
|
@ -356,10 +370,18 @@ pip install partftpy # tftp server
|
|||
pip install impacket # smb server -- disable Windows Defender if you REALLY need this on windows
|
||||
pip install Pillow pillow-heif # thumbnails
|
||||
pip install pyvips # faster thumbnails
|
||||
pip install psutil # better cleanup of stuck metadata parsers on windows
|
||||
pip install psutil # better cleanup of stuck metadata parsers on windows
|
||||
pip install black==21.12b0 click==8.0.2 bandit pylint flake8 isort mypy # vscode tooling
|
||||
```
|
||||
|
||||
* on archlinux you can do this:
|
||||
* `sudo pacman -Sy --needed python-{pip,isort,jinja,argon2-cffi,pyzmq,mutagen,pyftpdlib,pillow}`
|
||||
* then, as user: `python3 -m pip install --user --break-system-packages -U strip_hints black==21.12b0 click==8.0.2`
|
||||
* for building docker images: `sudo pacman -Sy --needed qemu-user-static{,-binfmt} podman{,-docker} jq`
|
||||
|
||||
* and if you want to run the python 2.7 tests:
|
||||
* `git clone https://github.com/pyenv/pyenv .pyenv ; cd .pyenv/bin ; env PYTHON_CONFIGURE_OPTS='--enable-optimizations' PYTHON_CFLAGS='-march=native -mtune=native -std=c17' ./pyenv install 2.7.18 -v ; ln -s $HOME/.pyenv/versions/2.7.18/bin/python2 $HOME/bin/`
|
||||
|
||||
|
||||
## just the sfx
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
services:
|
||||
---
|
||||
|
||||
services:
|
||||
copyparty:
|
||||
image: copyparty/ac:latest
|
||||
container_name: copyparty
|
||||
|
|
|
@ -8,7 +8,7 @@ to try this out with minimal adjustments:
|
|||
* login to https://fs.example.com/ with username `authelia` password `authelia`
|
||||
|
||||
to use this in a safe and secure manner:
|
||||
* follow a guide on setting up authelia properly (TODO:link) and use the copyparty-specific parts of this folder as inspiration for your own config; namely the `cpp` subfolder and the `copyparty` service in `docker-compose.yml`
|
||||
* follow a guide on setting up [authelia](https://www.authelia.com/integration/proxies/traefik/#docker-compose) properly and use the copyparty-specific parts of this folder as inspiration for your own config; namely the `cpp` subfolder and the `copyparty` service in `docker-compose.yml`
|
||||
|
||||
this folder is based on:
|
||||
* https://github.com/authelia/authelia/tree/39763aaed24c4abdecd884b47357a052b235942d/examples/compose/lite
|
||||
|
@ -16,20 +16,18 @@ this folder is based on:
|
|||
incomplete list of modifications made:
|
||||
* support for running with podman as root on fedora (`:z` volumes, `label:disable`)
|
||||
* explicitly using authelia `v4.38.0-beta3` because config syntax changed since last stable release
|
||||
* disabled automatic letsencrypt certificate signing
|
||||
* reduced logging from debug to info
|
||||
* added a warning that traefik is given access to the docker socket (as recommended by traefik docs) which means traefik is able to break out of the container and has full root access on the host machine
|
||||
* implemented a docker socket-proxy to not bind the docker.socket directly to traefik
|
||||
* using valkey instead of redis for caching
|
||||
|
||||
|
||||
# security
|
||||
|
||||
there is probably/definitely room for improvement in this example setup. Some ideas taken from [github issue #62](https://github.com/9001/copyparty/issues/62):
|
||||
|
||||
* Add in a redis password to limit attacker lateral movement in the system
|
||||
* Move redis to a private network shared with just authelia
|
||||
* Pin to image hashes (or go all in on updates and add `watchtower`)
|
||||
* Move valkey to a private network shared with just authelia
|
||||
* Add `watchtower` to manage your image version updates
|
||||
* Drop bridge networking for just exposing traefik's public ports
|
||||
* Configure docker for non-root access to docker socket and then move traefik to use [non-root perms](https://docs.docker.com/engine/security/rootless/)
|
||||
|
||||
if you manage to improve on any of this, especially in a way that might be useful for other people, consider sending a PR :>
|
||||
|
||||
|
@ -47,4 +45,4 @@ currently **not optimal,** at least when compared to running the python sfx outs
|
|||
|
||||
authelia is behaving strangely, handling 340 requests per second for a while, but then it suddenly drops to 75 and stays there...
|
||||
|
||||
I'm assuming all of the performance issues is due to a misconfiguration of authelia/traefik/docker on my end, but I don't relly know where to start
|
||||
I'm assuming all of the performance issues is due to a misconfiguration of authelia/traefik/docker on my end, but I don't really know where to start
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
# based on https://github.com/authelia/authelia/blob/39763aaed24c4abdecd884b47357a052b235942d/examples/compose/lite/authelia/configuration.yml
|
||||
|
||||
# Authelia configuration
|
||||
|
||||
# This secret can also be set using the env variables AUTHELIA_JWT_SECRET_FILE
|
||||
jwt_secret: a_very_important_secret
|
||||
identity_validation:
|
||||
reset_password:
|
||||
jwt_secret: 'a_very_important_secret_so_please_change_this'
|
||||
|
||||
server:
|
||||
address: 'tcp://:9091'
|
||||
|
||||
log:
|
||||
level: info # debug
|
||||
level: info
|
||||
|
||||
totp:
|
||||
issuer: authelia.com
|
||||
|
@ -21,29 +20,26 @@ authentication_backend:
|
|||
access_control:
|
||||
default_policy: deny
|
||||
rules:
|
||||
# Rules applied to everyone
|
||||
- domain: traefik.example.com
|
||||
policy: one_factor
|
||||
- domain: auth.example.com
|
||||
policy: bypass # Allow access to the login UI
|
||||
- domain: fs.example.com
|
||||
policy: one_factor
|
||||
|
||||
session:
|
||||
# This secret can also be set using the env variables AUTHELIA_SESSION_SECRET_FILE
|
||||
secret: unsecure_session_secret
|
||||
|
||||
cookies:
|
||||
- name: authelia_session
|
||||
domain: example.com # Should match whatever your root protected domain is
|
||||
domain: example.com # this should match whatever your root protected domain is
|
||||
default_redirection_url: https://fs.example.com
|
||||
authelia_url: https://authelia.example.com/
|
||||
expiration: 3600 # 1 hour
|
||||
inactivity: 300 # 5 minutes
|
||||
|
||||
redis:
|
||||
host: redis
|
||||
host: valkey
|
||||
port: 6379
|
||||
# This secret can also be set using the env variables AUTHELIA_SESSION_REDIS_PASSWORD_FILE
|
||||
# password: authelia
|
||||
password: your_secure_password_here
|
||||
|
||||
|
||||
regulation:
|
||||
max_retries: 3
|
||||
|
@ -58,9 +54,7 @@ storage:
|
|||
notifier:
|
||||
disable_startup_check: true
|
||||
smtp:
|
||||
username: test
|
||||
# This secret can also be set using the env variables AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE
|
||||
password: password
|
||||
host: mail.example.com
|
||||
port: 25
|
||||
sender: admin@example.com
|
||||
address: 'smtp://127.0.0.1:25'
|
||||
username: 'test'
|
||||
password: 'password'
|
||||
sender: "Authelia <admin@example.com>"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
version: '3.3'
|
||||
---
|
||||
|
||||
networks:
|
||||
net:
|
||||
|
@ -6,7 +6,7 @@ networks:
|
|||
|
||||
services:
|
||||
copyparty:
|
||||
image: copyparty/ac
|
||||
image: copyparty/ac:latest
|
||||
container_name: idp_copyparty
|
||||
user: "1000:1000" # should match the user/group of your fileshare volumes
|
||||
volumes:
|
||||
|
@ -19,19 +19,19 @@ services:
|
|||
labels:
|
||||
- 'traefik.enable=true'
|
||||
- 'traefik.http.routers.copyparty.rule=Host(`fs.example.com`)'
|
||||
- 'traefik.http.routers.copyparty.entrypoints=https'
|
||||
- 'traefik.http.routers.copyparty.entrypoints=websecure'
|
||||
- 'traefik.http.routers.copyparty.tls=true'
|
||||
- 'traefik.http.routers.copyparty.tls.certresolver=letsencrypt' # ← THIS IS CRUCIAL
|
||||
- 'traefik.http.routers.copyparty.middlewares=authelia@docker'
|
||||
stop_grace_period: 15s # thumbnailer is allowed to continue finishing up for 10s after the shutdown signal
|
||||
environment:
|
||||
LD_PRELOAD: /usr/lib/libmimalloc-secure.so.NOPE
|
||||
# enable mimalloc by replacing "NOPE" with "2" for a nice speed-boost (will use twice as much ram)
|
||||
|
||||
PYTHONUNBUFFERED: 1
|
||||
# ensures log-messages are not delayed (but can reduce speed a tiny bit)
|
||||
|
||||
authelia:
|
||||
image: authelia/authelia:v4.38.0-beta3 # the config files in the authelia folder use the new syntax
|
||||
image: authelia/authelia:4.39.5@sha256:023e02e5203dfa0ebaee7a48b5bae34f393d1f9cada4a9df7fbf87eb1759c671
|
||||
container_name: idp_authelia
|
||||
volumes:
|
||||
- ./authelia:/config:z
|
||||
|
@ -40,25 +40,23 @@ services:
|
|||
labels:
|
||||
- 'traefik.enable=true'
|
||||
- 'traefik.http.routers.authelia.rule=Host(`authelia.example.com`)'
|
||||
- 'traefik.http.routers.authelia.entrypoints=https'
|
||||
- 'traefik.http.routers.authelia.entrypoints=websecure'
|
||||
- 'traefik.http.routers.authelia.tls=true'
|
||||
#- 'traefik.http.routers.authelia.tls.certresolver=letsencrypt' # uncomment this to enable automatic certificate signing (1/2)
|
||||
- 'traefik.http.routers.authelia.tls.certresolver=letsencrypt'
|
||||
- 'traefik.http.middlewares.authelia.forwardauth.address=http://authelia:9091/api/authz/forward-auth?authelia_url=https://authelia.example.com'
|
||||
- 'traefik.http.middlewares.authelia.forwardauth.trustForwardHeader=true'
|
||||
- 'traefik.http.middlewares.authelia.forwardauth.authResponseHeaders=Remote-User,Remote-Groups,Remote-Name,Remote-Email'
|
||||
expose:
|
||||
- 9091
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
disable: true
|
||||
environment:
|
||||
- TZ=Etc/UTC
|
||||
|
||||
redis:
|
||||
image: redis:7.2.4-alpine3.19
|
||||
container_name: idp_redis
|
||||
valkey:
|
||||
image: valkey/valkey:8.1.3-alpine3.22@sha256:0d27f0bca0249f61d060029a6aaf2e16b2c417d68d02a508e1dfb763fa2948b4
|
||||
container_name: idp_valkey
|
||||
volumes:
|
||||
- ./redis:/data:z
|
||||
- ./valkey:/data:z
|
||||
networks:
|
||||
- net
|
||||
expose:
|
||||
|
@ -66,40 +64,55 @@ services:
|
|||
restart: unless-stopped
|
||||
environment:
|
||||
- TZ=Etc/UTC
|
||||
- VALKEY_EXTRA_FLAGS=--requirepass your_secure_password_here
|
||||
|
||||
socket-proxy:
|
||||
image: lscr.io/linuxserver/socket-proxy:3.2.3@sha256:63d2e0ce6bb0d12dfdbde5c3af31d08fee343ec3801a050c8197a3f5ffae8bed
|
||||
container_name: idp_socket_proxy
|
||||
environment:
|
||||
- CONTAINERS=1
|
||||
- NETWORKS=1
|
||||
- EVENTS=1
|
||||
- PING=1
|
||||
- VERSION=1
|
||||
- LOG_LEVEL=warning
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
read_only: true
|
||||
tmpfs:
|
||||
- /run
|
||||
networks:
|
||||
- net
|
||||
restart: unless-stopped
|
||||
expose:
|
||||
- 2375
|
||||
|
||||
traefik:
|
||||
image: traefik:2.11.0
|
||||
image: traefik:3.5.0@sha256:4e7175cfe19be83c6b928cae49dde2f2788fb307189a4dc9550b67acf30c11a5
|
||||
container_name: idp_traefik
|
||||
volumes:
|
||||
- ./traefik:/etc/traefik:z
|
||||
- /var/run/docker.sock:/var/run/docker.sock # WARNING: this gives traefik full root-access to the host OS, but is recommended/required(?) by traefik
|
||||
security_opt:
|
||||
- label:disable # disable selinux because it (rightly) blocks access to docker.sock
|
||||
networks:
|
||||
- net
|
||||
labels:
|
||||
- 'traefik.enable=true'
|
||||
- 'traefik.http.routers.api.rule=Host(`traefik.example.com`)'
|
||||
- 'traefik.http.routers.api.entrypoints=https'
|
||||
- 'traefik.http.routers.api.service=api@internal'
|
||||
- 'traefik.http.routers.api.tls=true'
|
||||
#- 'traefik.http.routers.api.tls.certresolver=letsencrypt' # uncomment this to enable automatic certificate signing (2/2)
|
||||
- 'traefik.http.routers.api.middlewares=authelia@docker'
|
||||
ports:
|
||||
- '80:80'
|
||||
- '443:443'
|
||||
command:
|
||||
- '--api'
|
||||
- '--providers.docker=true'
|
||||
- '--global.sendAnonymousUsage=false'
|
||||
- '--providers.docker.endpoint=tcp://socket-proxy:2375'
|
||||
- '--providers.docker.exposedByDefault=false'
|
||||
- '--entrypoints.http=true'
|
||||
- '--entrypoints.http.address=:80'
|
||||
- '--entrypoints.http.http.redirections.entrypoint.to=https'
|
||||
- '--entrypoints.http.http.redirections.entrypoint.scheme=https'
|
||||
- '--entrypoints.https=true'
|
||||
- '--entrypoints.https.address=:443'
|
||||
- '--entrypoints.web.address=:80'
|
||||
- '--entrypoints.web.http.redirections.entrypoint.to=websecure'
|
||||
- '--entrypoints.web.http.redirections.entrypoint.scheme=https'
|
||||
- '--entrypoints.websecure.address=:443'
|
||||
- '--certificatesResolvers.letsencrypt.acme.email=your-email@your-domain.com'
|
||||
- '--certificatesResolvers.letsencrypt.acme.storage=/etc/traefik/acme.json'
|
||||
- '--certificatesResolvers.letsencrypt.acme.httpChallenge.entryPoint=http'
|
||||
- '--log=true'
|
||||
- '--log.level=WARNING' # DEBUG
|
||||
- '--certificatesResolvers.letsencrypt.acme.httpChallenge.entryPoint=web'
|
||||
- '--log.level=INFO'
|
||||
depends_on:
|
||||
- socket-proxy
|
||||
|
|
|
@ -6,15 +6,15 @@ L: MIT
|
|||
|
||||
https://github.com/pallets/jinja/
|
||||
C: 2007 Pallets
|
||||
L: BSD 3-Clause
|
||||
L: BSD-3-Clause
|
||||
|
||||
https://github.com/pallets/markupsafe/
|
||||
C: 2010 Pallets
|
||||
L: BSD 3-Clause
|
||||
L: BSD-3-Clause
|
||||
|
||||
https://github.com/paulc/dnslib/
|
||||
C: 2010-2017 Paul Chakravarti
|
||||
L: BSD 2-Clause
|
||||
L: BSD-2-Clause
|
||||
|
||||
https://github.com/pydron/ifaddr/
|
||||
C: 2014 Stefan C. Mueller
|
||||
|
@ -36,6 +36,10 @@ https://github.com/ahupp/python-magic/
|
|||
C: 2001-2014 Adam Hupp
|
||||
L: MIT
|
||||
|
||||
https://github.com/fusepy/fusepy
|
||||
C: 2012 Giorgos Verigakis
|
||||
L: ISC
|
||||
|
||||
--- client-side --- software ---
|
||||
|
||||
https://github.com/Daninet/hash-wasm/
|
||||
|
@ -50,6 +54,10 @@ https://github.com/feimosi/baguetteBox.js/
|
|||
C: 2017 Marek Grzybek
|
||||
L: MIT
|
||||
|
||||
https://github.com/cure53/DOMPurify/
|
||||
C: 2025 Cure53 / Mario Heiderich
|
||||
L: Apache-2.0 or MPL-2.0
|
||||
|
||||
https://github.com/markedjs/marked/
|
||||
C: 2018+, MarkedJS
|
||||
C: 2011-2018, Christopher Jeffrey (https://github.com/chjj/)
|
||||
|
@ -68,8 +76,8 @@ L: MIT
|
|||
|
||||
https://github.com/adobe-fonts/source-code-pro/
|
||||
C: 2010-2019 Adobe
|
||||
L: SIL OFL 1.1
|
||||
L: OFL-1.1
|
||||
|
||||
https://github.com/FortAwesome/Font-Awesome/
|
||||
C: 2022 Fonticons, Inc.
|
||||
L: SIL OFL 1.1
|
||||
L: OFL-1.1
|
||||
|
|
|
@ -71,7 +71,7 @@ avg() { awk 'function pr(ncsz) {if (nsmp>0) {printf "%3s %s\n", csz, sum/nsmp} c
|
|||
python3 -um copyparty -nw -v srv::rw -i 127.0.0.1 2>&1 | tee log
|
||||
cat log | awk '!/"purl"/{next} {s=$1;sub(/[^m]+m/,"");gsub(/:/," ");t=60*(60*$1+$2)+$3} t<p{t+=86400} !a{a=t;sa=s} {b=t;sb=s} END {print b-a,sa,sb}'
|
||||
|
||||
# or if the client youre measuring dies for ~15sec every once ina while and you wanna filter those out,
|
||||
# or if the client you're measuring dies for ~15sec every once ina while and you wanna filter those out,
|
||||
cat log | awk '!/"purl"/{next} {s=$1;sub(/[^m]+m/,"");gsub(/:/," ");t=60*(60*$1+$2)+$3} t<p{t+=86400} !p{a=t;p=t;r=0;next} t-p>1{printf "%.3f += %.3f - %.3f (%.3f) # %.3f -> %.3f\n",r,p,a,p-a,p,t;r+=p-a;a=t} {p=t} END {print r+p-a}'
|
||||
|
||||
|
||||
|
@ -337,3 +337,5 @@ mk && t0="$(date)" && while true; do date -s "$(date '+ 1 hour')"; systemd-tmpfi
|
|||
mk && sudo -u ed flock /tmp/foo sleep 40 & sleep 1; ps aux | grep -E 'sleep 40$' && t0="$(date)" && for n in {1..40}; do date -s "$(date '+ 1 day')"; systemd-tmpfiles --clean; ls -1 /tmp | grep foo || break; done; echo "$t0"
|
||||
mk && t0="$(date)" && for n in {1..40}; do date -s "$(date '+ 1 day')"; systemd-tmpfiles --clean; ls -1 /tmp | grep foo || break; tar -cf/dev/null /tmp/foo; done; echo "$t0"
|
||||
|
||||
# number of megabytes downloaded since some date
|
||||
awk </var/log/wjaycore.out '/^..36m2025-05-20/{o=1} !o{next} !/ plain 20[06](,| \[[^,]+\],) +[0-9.]+.\[33m[KM] .* n[0-9]+$/{next} {v=$0;sub(/.* plain 20[06](,| \[[^,]+\],) +/,"",v);sub(/ .*/,"",v);u=v;sub(/.\[.*/,"",v);sub(/.*m/,"",u);$0=u} /[KMG]/{v*=1024} /[MG]/{v*=1024} /G/{v*=1024} {t+=v} END{printf "%d\n",t/(1024*1024)}'
|
||||
|
|
|
@ -64,6 +64,8 @@ if you give it the value `@ASDF` it will try to open a file named ASDF and send
|
|||
|
||||
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)
|
||||
|
||||
there is also `--html-head-s` and volflag `html_head_s` to add a plain static bit of text, possibly in addition to `--html-head`
|
||||
|
||||
|
||||
# translations
|
||||
|
||||
|
|
139
docs/versus.md
139
docs/versus.md
|
@ -68,7 +68,7 @@ currently up to date with [awesome-selfhosted](https://github.com/awesome-selfho
|
|||
* [kodbox](https://github.com/kalcaddle/kodbox) ([review](#kodbox)) appears to be a fantastic alternative if you're not worried about running chinese software, with several advantages over copyparty
|
||||
* but anything you want to share must be moved into the kodbox filesystem
|
||||
* [seafile](https://github.com/haiwen/seafile) ([review](#seafile)) and [nextcloud](https://github.com/nextcloud/server) ([review](#nextcloud)) could be decent alternatives if you need something heavier than copyparty
|
||||
* but their [license](https://snyk.io/learn/agpl-license/) is [problematic](https://opensource.google/documentation/reference/using/agpl-policy)
|
||||
* but their [license (AGPL)](https://snyk.io/learn/agpl-license/) is [thorny](https://opensource.google/documentation/reference/using/agpl-policy)
|
||||
* and copyparty is way better at uploads in particular (resumable, accelerated)
|
||||
* and anything you want to share must be moved into the respective filesystems
|
||||
* [filebrowser](https://github.com/filebrowser/filebrowser) ([review](#filebrowser)) and [dufs](https://github.com/sigoden/dufs) ([review](#dufs)) are simpler copyparties but with a settings gui
|
||||
|
@ -85,19 +85,34 @@ currently up to date with [awesome-selfhosted](https://github.com/awesome-selfho
|
|||
the table headers in the matrixes below are the different softwares, with a quick review of each software in the next section
|
||||
|
||||
the softwares,
|
||||
* `a` = [copyparty](https://github.com/9001/copyparty)
|
||||
* `b` = [hfs2](https://github.com/rejetto/hfs2/) 🔥
|
||||
* `c` = [hfs3](https://rejetto.com/hfs/)
|
||||
* `d` = [nextcloud](https://github.com/nextcloud/server)
|
||||
* `e` = [seafile](https://github.com/haiwen/seafile)
|
||||
* `f` = [rclone](https://github.com/rclone/rclone), specifically `rclone serve webdav .`
|
||||
* `g` = [dufs](https://github.com/sigoden/dufs)
|
||||
* `h` = [chibisafe](https://github.com/chibisafe/chibisafe)
|
||||
* `i` = [kodbox](https://github.com/kalcaddle/kodbox)
|
||||
* `j` = [filebrowser](https://github.com/filebrowser/filebrowser)
|
||||
* `k` = [filegator](https://github.com/filegator/filegator)
|
||||
* `l` = [sftpgo](https://github.com/drakkan/sftpgo)
|
||||
* `m` = [arozos](https://github.com/tobychui/arozos)
|
||||
|
||||
[a]: https://github.com/9001/copyparty "copyparty"
|
||||
[b]: https://github.com/rejetto/hfs2/ "hfs2"
|
||||
[c]: https://rejetto.com/hfs/ "hfs3"
|
||||
[d]: https://github.com/nextcloud/server "nextcloud"
|
||||
[e]: https://github.com/haiwen/seafile "seafile"
|
||||
[f]: https://github.com/rclone/rclone "rclone"
|
||||
[g]: https://github.com/sigoden/dufs "dufs"
|
||||
[h]: https://github.com/chibisafe/chibisafe "chibisafe"
|
||||
[i]: https://github.com/kalcaddle/kodbox "kodbox"
|
||||
[j]: https://github.com/filebrowser/filebrowser "filebrowser"
|
||||
[k]: https://github.com/filegator/filegator "filegator"
|
||||
[l]: https://github.com/drakkan/sftpgo "sftpgo"
|
||||
[m]: https://github.com/tobychui/arozos "arozos"
|
||||
|
||||
* `a` = [copyparty][a]
|
||||
* `b` = [hfs2][b] 🔥
|
||||
* `c` = [hfs3][c]
|
||||
* `d` = [nextcloud][d]
|
||||
* `e` = [seafile][e]
|
||||
* `f` = [rclone][f], specifically `rclone serve webdav .`
|
||||
* `g` = [dufs][g]
|
||||
* `h` = [chibisafe][h]
|
||||
* `i` = [kodbox][i]
|
||||
* `j` = [filebrowser][j]
|
||||
* `k` = [filegator][k]
|
||||
* `l` = [sftpgo][l]
|
||||
* `m` = [arozos][m]
|
||||
|
||||
some softwares not in the matrixes,
|
||||
* [updog](#updog)
|
||||
|
@ -119,18 +134,18 @@ symbol legend,
|
|||
|
||||
## general
|
||||
|
||||
| feature / software | a | b | c | d | e | f | g | h | i | j | k | l | m |
|
||||
| feature / software |[a]|[b]|[c]|[d]|[e]|[f]|[g]|[h]|[i]|[j]|[k]|[l]|[m]|
|
||||
| ----------------------- | - | - | - | - | - | - | - | - | - | - | - | - | - |
|
||||
| intuitive UX | | ╱ | █ | █ | █ | | █ | █ | █ | █ | █ | █ | █ |
|
||||
| config GUI | | █ | █ | █ | █ | | | █ | █ | █ | | █ | █ |
|
||||
| good documentation | | | | █ | █ | █ | █ | | | █ | █ | ╱ | ╱ |
|
||||
| good documentation | | | █ | █ | █ | █ | █ | | | █ | █ | ╱ | ╱ |
|
||||
| runs on iOS | ╱ | | | | | ╱ | | | | | | | |
|
||||
| runs on Android | █ | | | | | █ | | | | | | | |
|
||||
| runs on Android | █ | | █ | | | █ | | | | | | █ | |
|
||||
| runs on WinXP | █ | █ | | | | █ | | | | | | | |
|
||||
| runs on Windows | █ | █ | █ | █ | █ | █ | █ | ╱ | █ | █ | █ | █ | ╱ |
|
||||
| runs on Linux | █ | ╱ | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ |
|
||||
| runs on Macos | █ | | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | |
|
||||
| runs on FreeBSD | █ | | | • | █ | █ | █ | • | █ | █ | | █ | |
|
||||
| runs on FreeBSD | █ | | █ | • | █ | █ | █ | • | █ | █ | | █ | |
|
||||
| runs on Risc-V | █ | | | █ | █ | █ | | • | | █ | | | |
|
||||
| portable binary | █ | █ | █ | | | █ | █ | | | █ | | █ | █ |
|
||||
| zero setup, just go | █ | █ | █ | | | ╱ | █ | | | █ | | ╱ | █ |
|
||||
|
@ -140,13 +155,15 @@ symbol legend,
|
|||
* `zero setup` = you can get a mostly working setup by just launching the app, without having to install any software or configure whatever
|
||||
* `a`/copyparty remarks:
|
||||
* no gui for server settings; only for client-side stuff
|
||||
* can theoretically run on iOS / iPads using [iSH](https://ish.app/), but only the iPad will offer sufficient multitasking i think
|
||||
* runs on iOS / iPads using [a-Shell](https://holzschu.github.io/a-Shell_iOS/) (pretty good) or [iSH](https://ish.app/) (very slow) but cannot run in the background and is not able to share all of your phone storage (just a separate dedicated folder)
|
||||
* [android app](https://f-droid.org/en/packages/me.ocv.partyup/) is for uploading only
|
||||
* no iOS app but has [shortcuts](https://github.com/9001/copyparty#ios-shortcuts) for easy uploading
|
||||
* `b`/hfs2 runs on linux through wine
|
||||
* `f`/rclone must be started with the command `rclone serve webdav .` or similar
|
||||
* `h`/chibisafe has undocumented windows support
|
||||
* `i`/sftpgo must be launched with a command
|
||||
* `l`/sftpgo:
|
||||
* Must be launched with a command
|
||||
* On Termux, just run `pkg in sftpgo`
|
||||
* `m`/arozos has partial windows support
|
||||
|
||||
|
||||
|
@ -154,14 +171,14 @@ symbol legend,
|
|||
|
||||
*the thing that copyparty is actually kinda good at*
|
||||
|
||||
| feature / software | a | b | c | d | e | f | g | h | i | j | k | l | m |
|
||||
| feature / software |[a]|[b]|[c]|[d]|[e]|[f]|[g]|[h]|[i]|[j]|[k]|[l]|[m]|
|
||||
| ----------------------- | - | - | - | - | - | - | - | - | - | - | - | - | - |
|
||||
| download folder as zip | █ | █ | █ | █ | ╱ | | █ | | █ | █ | ╱ | █ | ╱ |
|
||||
| download folder as tar | █ | | | | | | | | | | | | |
|
||||
| upload | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | ╱ | █ | █ |
|
||||
| parallel uploads | █ | | | █ | █ | | • | | █ | █ | █ | | █ |
|
||||
| resumable uploads | █ | | █ | | | | | | █ | █ | █ | ╱ | |
|
||||
| upload segmenting | █ | | | █ | | | | █ | █ | █ | █ | ╱ | █ |
|
||||
| upload segmenting | █ | | █ | █ | | | | █ | █ | █ | █ | ╱ | █ |
|
||||
| upload acceleration | █ | | | | | | | | █ | | █ | | |
|
||||
| upload verification | █ | | | █ | █ | | | | █ | | | | |
|
||||
| upload deduplication | █ | | | | █ | | | | █ | | | | |
|
||||
|
@ -169,7 +186,7 @@ symbol legend,
|
|||
| CTRL-V from device | █ | | | █ | | | | | | | | | |
|
||||
| race the beam ("p2p") | █ | | | | | | | | | | | | |
|
||||
| "tail -f" streaming | █ | | | | | | | | | | | | |
|
||||
| keep last-modified time | █ | | | █ | █ | █ | | | | | | █ | |
|
||||
| keep last-modified time | █ | | █ | █ | █ | █ | | | | | | █ | |
|
||||
| upload rules | ╱ | ╱ | ╱ | ╱ | ╱ | | | ╱ | ╱ | | ╱ | ╱ | ╱ |
|
||||
| ┗ max disk usage | █ | █ | █ | | █ | | | | █ | | | █ | █ |
|
||||
| ┗ max filesize | █ | | | | | | | █ | | | █ | █ | █ |
|
||||
|
@ -218,7 +235,7 @@ symbol legend,
|
|||
|
||||
## protocols and client support
|
||||
|
||||
| feature / software | a | b | c | d | e | f | g | h | i | j | k | l | m |
|
||||
| feature / software |[a]|[b]|[c]|[d]|[e]|[f]|[g]|[h]|[i]|[j]|[k]|[l]|[m]|
|
||||
| ----------------------- | - | - | - | - | - | - | - | - | - | - | - | - | - |
|
||||
| serve https | █ | | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ |
|
||||
| serve webdav | █ | | | █ | █ | █ | █ | | █ | | | █ | █ |
|
||||
|
@ -249,9 +266,9 @@ symbol legend,
|
|||
|
||||
## server configuration
|
||||
|
||||
| feature / software | a | b | c | d | e | f | g | h | i | j | k | l | m |
|
||||
| feature / software |[a]|[b]|[c]|[d]|[e]|[f]|[g]|[h]|[i]|[j]|[k]|[l]|[m]|
|
||||
| ----------------------- | - | - | - | - | - | - | - | - | - | - | - | - | - |
|
||||
| config from cmd args | █ | | | | | █ | █ | | | █ | | ╱ | ╱ |
|
||||
| config from cmd args | █ | | █ | | | █ | █ | | | █ | | ╱ | ╱ |
|
||||
| config files | █ | █ | █ | ╱ | ╱ | █ | | █ | | █ | • | ╱ | ╱ |
|
||||
| runtime config reload | █ | █ | █ | | | | | █ | █ | █ | █ | | █ |
|
||||
| same-port http / https | █ | | | | | | | | | | | | |
|
||||
|
@ -262,7 +279,7 @@ symbol legend,
|
|||
|
||||
* `folder-rproxy` = reverse-proxying without dedicating an entire (sub)domain, using a subfolder instead
|
||||
* `l`/sftpgo:
|
||||
* config: users must be added through gui / api calls
|
||||
* config: user can be added by cmd command in [Portable mode](https://docs.sftpgo.com/2.6/cli/#portable-mode); if not in Portable mode users must be added through gui / api calls
|
||||
* `m`/arozos:
|
||||
* configuration is primarily through GUI
|
||||
* reverse-proxy is not guaranteed to see the correct client IP
|
||||
|
@ -270,17 +287,17 @@ symbol legend,
|
|||
|
||||
## server capabilities
|
||||
|
||||
| feature / software | a | b | c | d | e | f | g | h | i | j | k | l | m |
|
||||
| feature / software |[a]|[b]|[c]|[d]|[e]|[f]|[g]|[h]|[i]|[j]|[k]|[l]|[m]|
|
||||
| ----------------------- | - | - | - | - | - | - | - | - | - | - | - | - | - |
|
||||
| accounts | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ |
|
||||
| per-account chroot | | | | | | | | | | | | █ | |
|
||||
| single-sign-on | ╱ | | | █ | █ | | | | • | | | | |
|
||||
| token auth | ╱ | | | █ | █ | | | █ | | | | | █ |
|
||||
| 2fa | ╱ | | | █ | █ | | | | | | | █ | ╱ |
|
||||
| 2fa | ╱ | | / | █ | █ | | | | | | | █ | ╱ |
|
||||
| per-volume permissions | █ | █ | █ | █ | █ | █ | █ | | █ | █ | ╱ | █ | █ |
|
||||
| per-folder permissions | ╱ | | | █ | █ | | █ | | █ | █ | ╱ | █ | █ |
|
||||
| per-file permissions | | | | █ | █ | | █ | | █ | | | | █ |
|
||||
| per-file passwords | █ | | | █ | █ | | █ | | █ | | | | █ |
|
||||
| per-folder permissions | ╱ | | █ | █ | █ | | █ | | █ | █ | ╱ | █ | █ |
|
||||
| per-file permissions | | | █ | █ | █ | | █ | | █ | | | | █ |
|
||||
| per-file passwords | █ | | | █ | █ | | █ | | █ | | | █ | █ |
|
||||
| unmap subfolders | █ | | █ | | | | █ | | | █ | ╱ | • | |
|
||||
| index.html blocks list | ╱ | | | | | | █ | | | • | | | |
|
||||
| write-only folders | █ | | █ | | █ | | | | | | █ | █ | |
|
||||
|
@ -297,13 +314,13 @@ symbol legend,
|
|||
| full sync | | | | █ | █ | | | | | | | | |
|
||||
| speed throttle | | █ | █ | | | █ | | | █ | | | █ | |
|
||||
| anti-bruteforce | █ | █ | █ | █ | █ | | | | • | | | █ | • |
|
||||
| dyndns updater | | █ | | | | | | | | | | | |
|
||||
| dyndns updater | | █ | █ | | | | | | | | | | |
|
||||
| self-updater | | | █ | | | | | | | | | | █ |
|
||||
| log rotation | █ | | █ | █ | █ | | | • | █ | | | █ | • |
|
||||
| upload tracking / log | █ | █ | • | █ | █ | | | █ | █ | | | ╱ | █ |
|
||||
| prometheus metrics | █ | | | █ | | | | | | | | █ | |
|
||||
| curl-friendly ls | █ | | | | | | | | | | | | |
|
||||
| curl-friendly upload | █ | | | | | █ | █ | • | | | | | |
|
||||
| curl-friendly upload | █ | | █ | | | █ | █ | • | | | | | |
|
||||
|
||||
* `unmap subfolders` = "shadowing"; mounting a local folder in the middle of an existing filesystem tree in order to disable access below that path
|
||||
* `files stored as-is` = uploaded files are trivially readable from the server HDD, not sliced into chunks or in weird folder structures or anything like that
|
||||
|
@ -332,28 +349,29 @@ symbol legend,
|
|||
* `upload tracking / log` in main logfile
|
||||
* `m`/arozos:
|
||||
* `2fa` maybe possible through LDAP/Oauth
|
||||
|
||||
* `c`/hfs3
|
||||
* `2fa` available by installing a plugin
|
||||
|
||||
## client features
|
||||
|
||||
| feature / software | a | b | c | d | e | f | g | h | i | j | k | l | m |
|
||||
| feature / software |[a]|[b]|[c]|[d]|[e]|[f]|[g]|[h]|[i]|[j]|[k]|[l]|[m]|
|
||||
| ---------------------- | - | - | - | - | - | - | - | - | - | - | - | - | - |
|
||||
| single-page app | █ | | █ | █ | █ | | | █ | █ | █ | █ | | █ |
|
||||
| themes | █ | █ | █ | █ | | | | | █ | | | | |
|
||||
| directory tree nav | █ | ╱ | | | █ | | | | █ | | ╱ | | |
|
||||
| multi-column sorting | █ | | | | | | | | | | | | |
|
||||
| thumbnails | █ | | | ╱ | ╱ | | | █ | █ | ╱ | | | █ |
|
||||
| ┗ image thumbnails | █ | | | █ | █ | | | █ | █ | █ | | | █ |
|
||||
| multi-column sorting | █ | | | | | | | | | | | █ | |
|
||||
| thumbnails | █ | | / | ╱ | ╱ | | | █ | █ | ╱ | | | █ |
|
||||
| ┗ image thumbnails | █ | | / | █ | █ | | | █ | █ | █ | | | █ |
|
||||
| ┗ video thumbnails | █ | | | █ | █ | | | | █ | | | | █ |
|
||||
| ┗ audio spectrograms | █ | | | | | | | | | | | | |
|
||||
| audio player | █ | | ╱ | █ | █ | | | | █ | ╱ | | | █ |
|
||||
| audio player | █ | | ╱ | █ | █ | | | | █ | ╱ | | ╱ | █ |
|
||||
| ┗ gapless playback | █ | | | | | | | | • | | | | |
|
||||
| ┗ audio equalizer | █ | | | | | | | | | | | | |
|
||||
| ┗ waveform seekbar | █ | | | | | | | | | | | | |
|
||||
| ┗ OS integration | █ | | | | | | | | | | | | |
|
||||
| ┗ OS integration | █ | | █ | | | | | | | | | | |
|
||||
| ┗ transcode to lossy | █ | | | | | | | | | | | | |
|
||||
| video player | █ | | | █ | █ | | | | █ | █ | | | █ |
|
||||
| ┗ video transcoding | | | | | | | | | █ | | | | |
|
||||
| video player | █ | | █ | █ | █ | | | | █ | █ | | ╱ | █ |
|
||||
| ┗ video transcoding | | | / | | | | | | █ | | | | |
|
||||
| audio BPM detector | █ | | | | | | | | | | | | |
|
||||
| audio key detector | █ | | | | | | | | | | | | |
|
||||
| search by path / name | █ | █ | █ | █ | █ | | █ | | █ | █ | ╱ | | |
|
||||
|
@ -365,16 +383,16 @@ symbol legend,
|
|||
| find local file | █ | | | | | | | | | | | | |
|
||||
| undo recent uploads | █ | | | | | | | | | | | | |
|
||||
| create directories | █ | | █ | █ | █ | ╱ | █ | █ | █ | █ | █ | █ | █ |
|
||||
| image viewer | █ | | █ | █ | █ | | | | █ | █ | █ | | █ |
|
||||
| markdown viewer | █ | | | | █ | | | | █ | ╱ | ╱ | | █ |
|
||||
| markdown editor | █ | | | | █ | | | | █ | ╱ | ╱ | | █ |
|
||||
| readme.md in listing | █ | | | █ | | | | | | | | | |
|
||||
| image viewer | █ | | █ | █ | █ | | | | █ | █ | █ | █ | █ |
|
||||
| markdown viewer | █ | | / | | █ | | | | █ | ╱ | ╱ | | █ |
|
||||
| markdown editor | █ | | | | █ | | | | █ | ╱ | ╱ | ╱ | █ |
|
||||
| readme.md in listing | █ | | / | █ | | | | | | | | | |
|
||||
| rename files | █ | █ | █ | █ | █ | ╱ | █ | | █ | █ | █ | █ | █ |
|
||||
| batch rename | █ | | | | | | | | █ | | | | |
|
||||
| cut / paste files | █ | █ | | █ | █ | | | | █ | | | | █ |
|
||||
| move files | █ | █ | █ | █ | █ | | █ | | █ | █ | █ | | █ |
|
||||
| cut / paste files | █ | █ | █ | █ | █ | | | | █ | | | | █ |
|
||||
| move files | █ | █ | █ | █ | █ | | █ | | █ | █ | █ | █ | █ |
|
||||
| delete files | █ | █ | █ | █ | █ | ╱ | █ | █ | █ | █ | █ | █ | █ |
|
||||
| copy files | | | | | █ | | | | █ | █ | █ | | █ |
|
||||
| copy files | | | / | | █ | | | | █ | █ | █ | █ | █ |
|
||||
|
||||
* `single-page app` = multitasking; possible to continue navigating while uploading
|
||||
* `audio player » os-integration` = use the [lockscreen](https://user-images.githubusercontent.com/241032/142711926-0700be6c-3e31-47b3-9928-53722221f722.png) or [media hotkeys](https://user-images.githubusercontent.com/241032/215347492-b4250797-6c90-4e09-9a4c-721edf2fb15c.png) to play/pause, prev/next song
|
||||
|
@ -383,22 +401,23 @@ symbol legend,
|
|||
* `undo recent uploads` = accounts without delete permissions have a time window where they can undo their own uploads
|
||||
* `a`/copyparty has teeny-tiny skips playing gapless albums depending on audio codec (opus best)
|
||||
* `b`/hfs2 has a very basic directory tree view, not showing sibling folders
|
||||
* `c`/hfs3 remarks:
|
||||
* audio playback does not continue into next song
|
||||
* `f`/rclone can do some file management (mkdir, rename, delete) when hosting througn webdav
|
||||
* `j`/filebrowser remarks:
|
||||
* audio playback does not continue into next song
|
||||
* plaintext viewer/editor
|
||||
* `k`/filegator directory tree is a modal window
|
||||
* `l`/sftpgo remarks:
|
||||
* audio/video playback does not continue into next song/video
|
||||
* plaintext viewer/editor
|
||||
|
||||
|
||||
## integration
|
||||
|
||||
| feature / software | a | b | c | d | e | f | g | h | i | j | k | l | m |
|
||||
| feature / software |[a]|[b]|[c]|[d]|[e]|[f]|[g]|[h]|[i]|[j]|[k]|[l]|[m]|
|
||||
| ----------------------- | - | - | - | - | - | - | - | - | - | - | - | - | - |
|
||||
| OS alert on upload | █ | | | | | | | | | ╱ | | ╱ | |
|
||||
| discord | █ | | | | | | | | | ╱ | | ╱ | |
|
||||
| ┗ announce uploads | █ | | | | | | | | | | | ╱ | |
|
||||
| OS alert on upload | ╱ | | | | | | | | | ╱ | | ╱ | |
|
||||
| discord | ╱ | | | | | | | | | ╱ | | ╱ | |
|
||||
| ┗ announce uploads | ╱ | | | | | | | | | | | ╱ | |
|
||||
| ┗ custom embeds | | | | | | | | | | | | ╱ | |
|
||||
| sharex | █ | | | █ | | █ | ╱ | █ | | | | | |
|
||||
| flameshot | | | | | | █ | | | | | | | |
|
||||
|
@ -471,10 +490,8 @@ symbol legend,
|
|||
* vfs with gui config, per-volume permissions
|
||||
* tested locally, v0.53.2 on archlinux
|
||||
* 🔵 uploads are resumable
|
||||
* ⚠️ uploads are not segmented; max upload size 100 MiB on cloudflare
|
||||
* ⚠️ uploads are not accelerated (copyparty is 3x faster across the atlantic)
|
||||
* ⚠️ uploads are not integrity-checked
|
||||
* ⚠️ copies the file after upload; need twice filesize free disk space
|
||||
* ⚠️ uploading small files is decent; `107` files per sec (copyparty does `670`/sec, 6x faster)
|
||||
* ⚠️ doesn't support crazy filenames
|
||||
* ✅ config GUI
|
||||
|
@ -575,7 +592,7 @@ symbol legend,
|
|||
* ✅ file tags; file discussions!?
|
||||
* ✅ video transcoding
|
||||
* ✅ unzip uploaded archives
|
||||
* ✅ IDE with syntax hilighting
|
||||
* ✅ IDE with syntax highlighting
|
||||
* ✅ wysiwyg editor for openoffice files
|
||||
|
||||
## [filebrowser](https://github.com/filebrowser/filebrowser)
|
||||
|
@ -617,10 +634,8 @@ symbol legend,
|
|||
* ⚠️ across the atlantic, copyparty is 2.5x faster
|
||||
* 🔵 sftp uploads are resumable
|
||||
* ⚠️ web UI is very minimal + a bit slow
|
||||
* ⚠️ no thumbnails / image viewer / audio player
|
||||
* ⚠️ basic file manager (no cut/paste/move)
|
||||
* ⚠️ no thumbnails
|
||||
* ⚠️ no filesystem indexing / search
|
||||
* ⚠️ doesn't run on phones, tablets
|
||||
* ⚠️ no zeroconf (mdns/ssdp)
|
||||
* ⚠️ impractical directory URLs
|
||||
* ⚠️ AGPL licensed
|
||||
|
|
11
flake.nix
11
flake.nix
|
@ -12,16 +12,7 @@
|
|||
}:
|
||||
{
|
||||
nixosModules.default = ./contrib/nixos/modules/copyparty.nix;
|
||||
overlays.default = final: prev: {
|
||||
copyparty = final.python3.pkgs.callPackage ./contrib/package/nix/copyparty {
|
||||
ffmpeg = final.ffmpeg-full;
|
||||
};
|
||||
python3 = prev.python3.override {
|
||||
packageOverrides = pyFinal: pyPrev: {
|
||||
partftpy = pyFinal.callPackage ./contrib/package/nix/partftpy { };
|
||||
};
|
||||
};
|
||||
};
|
||||
overlays.default = import ./contrib/package/nix/overlay.nix;
|
||||
}
|
||||
// flake-utils.lib.eachDefaultSystem (
|
||||
system:
|
||||
|
|
|
@ -93,6 +93,7 @@ copyparty = [
|
|||
"web/*.js",
|
||||
"web/*.css",
|
||||
"web/*.html",
|
||||
"web/*.xml",
|
||||
"web/a/*.bat",
|
||||
"web/deps/*.gz",
|
||||
"web/deps/*.woff*",
|
||||
|
|
|
@ -3,7 +3,7 @@ WORKDIR /z
|
|||
ENV ver_asmcrypto=c72492f4a66e17a0e5dd8ad7874de354f3ccdaa5 \
|
||||
ver_hashwasm=4.12.0 \
|
||||
ver_marked=4.3.0 \
|
||||
ver_dompf=3.2.6 \
|
||||
ver_dompf=3.2.7 \
|
||||
ver_mde=2.18.0 \
|
||||
ver_codemirror=5.65.18 \
|
||||
ver_fontawesome=5.13.0 \
|
||||
|
@ -41,25 +41,25 @@ RUN mkdir -p /z/dist/no-pk \
|
|||
&& (mkdir hash-wasm \
|
||||
&& cd hash-wasm \
|
||||
&& unzip ../hash-wasm.zip) \
|
||||
&& (tar -xf asmcrypto.tgz \
|
||||
&& (tar --no-same-owner -xf asmcrypto.tgz \
|
||||
&& cd asmcrypto.js-$ver_asmcrypto \
|
||||
&& npm install ) \
|
||||
&& (tar -xf marked.tgz \
|
||||
&& (tar --no-same-owner -xf marked.tgz \
|
||||
&& cd marked-$ver_marked \
|
||||
&& npm install \
|
||||
&& npm i grunt uglify-js -g ) \
|
||||
&& (tar -xf codemirror.tgz \
|
||||
&& (tar --no-same-owner -xf codemirror.tgz \
|
||||
&& cd codemirror5-$ver_codemirror \
|
||||
&& npm install ) \
|
||||
&& (tar -xf mde.tgz \
|
||||
&& (tar --no-same-owner -xf mde.tgz \
|
||||
&& cd easy-markdown-editor* \
|
||||
&& npm install \
|
||||
&& npm i gulp-cli -g ) \
|
||||
&& tar -xf dompurify.tgz \
|
||||
&& tar -xf prism.tgz \
|
||||
&& tar -xf fusepy.tgz \
|
||||
&& tar --no-same-owner -xf dompurify.tgz \
|
||||
&& tar --no-same-owner -xf prism.tgz \
|
||||
&& tar --no-same-owner -xf fusepy.tgz \
|
||||
&& unzip fontawesome.zip \
|
||||
&& tar -xf zopfli.tgz
|
||||
&& tar --no-same-owner -xf zopfli.tgz
|
||||
|
||||
|
||||
COPY busy-mp3.sh /z/
|
||||
|
@ -68,7 +68,7 @@ RUN /z/busy-mp3.sh \
|
|||
|
||||
|
||||
# build fonttools (which needs zopfli)
|
||||
RUN tar -xf zopfli.tgz \
|
||||
RUN tar --no-same-owner -xf zopfli.tgz \
|
||||
&& cd zopfli* \
|
||||
&& cmake \
|
||||
-DCMAKE_INSTALL_PREFIX=/usr \
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
FROM alpine:latest
|
||||
WORKDIR /z
|
||||
LABEL org.opencontainers.image.url="https://github.com/9001/copyparty" \
|
||||
org.opencontainers.image.source="https://github.com/9001/copyparty/tree/hovudstraum/scripts/docker" \
|
||||
org.opencontainers.image.source="https://github.com/9001/copyparty" \
|
||||
org.opencontainers.image.licenses="MIT" \
|
||||
org.opencontainers.image.title="copyparty-ac" \
|
||||
org.opencontainers.image.description="copyparty with Pillow and FFmpeg (image/audio/video thumbnails, audio transcoding, media tags)"
|
||||
|
@ -16,6 +16,6 @@ COPY i/dist/copyparty-sfx.py innvikler.sh ./
|
|||
ADD base ./base
|
||||
RUN ash innvikler.sh ac
|
||||
|
||||
WORKDIR /w
|
||||
WORKDIR /state
|
||||
EXPOSE 3923
|
||||
ENTRYPOINT ["python3", "-m", "copyparty", "--no-crt", "-c", "/z/initcfg"]
|
||||
ENTRYPOINT ["python3", "-m", "copyparty", "-c", "/z/initcfg"]
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
FROM alpine:latest
|
||||
WORKDIR /z
|
||||
LABEL org.opencontainers.image.url="https://github.com/9001/copyparty" \
|
||||
org.opencontainers.image.source="https://github.com/9001/copyparty/tree/hovudstraum/scripts/docker" \
|
||||
org.opencontainers.image.source="https://github.com/9001/copyparty" \
|
||||
org.opencontainers.image.licenses="MIT" \
|
||||
org.opencontainers.image.title="copyparty-dj" \
|
||||
org.opencontainers.image.description="copyparty with all optional dependencies, including musical key / bpm detection"
|
||||
|
@ -38,6 +38,6 @@ COPY i/dist/copyparty-sfx.py innvikler.sh ./
|
|||
ADD base ./base
|
||||
RUN ash innvikler.sh dj
|
||||
|
||||
WORKDIR /w
|
||||
WORKDIR /state
|
||||
EXPOSE 3923
|
||||
ENTRYPOINT ["python3", "-m", "copyparty", "--no-crt", "-c", "/z/initcfg"]
|
||||
ENTRYPOINT ["python3", "-m", "copyparty", "-c", "/z/initcfg"]
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
FROM alpine:latest
|
||||
WORKDIR /z
|
||||
LABEL org.opencontainers.image.url="https://github.com/9001/copyparty" \
|
||||
org.opencontainers.image.source="https://github.com/9001/copyparty/tree/hovudstraum/scripts/docker" \
|
||||
org.opencontainers.image.source="https://github.com/9001/copyparty" \
|
||||
org.opencontainers.image.licenses="MIT" \
|
||||
org.opencontainers.image.title="copyparty-im" \
|
||||
org.opencontainers.image.description="copyparty with Pillow and Mutagen (image thumbnails, media tags)"
|
||||
|
@ -15,6 +15,6 @@ COPY i/dist/copyparty-sfx.py innvikler.sh ./
|
|||
ADD base ./base
|
||||
RUN ash innvikler.sh im
|
||||
|
||||
WORKDIR /w
|
||||
WORKDIR /state
|
||||
EXPOSE 3923
|
||||
ENTRYPOINT ["python3", "-m", "copyparty", "--no-crt", "-c", "/z/initcfg"]
|
||||
ENTRYPOINT ["python3", "-m", "copyparty", "-c", "/z/initcfg"]
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
FROM alpine:latest
|
||||
WORKDIR /z
|
||||
LABEL org.opencontainers.image.url="https://github.com/9001/copyparty" \
|
||||
org.opencontainers.image.source="https://github.com/9001/copyparty/tree/hovudstraum/scripts/docker" \
|
||||
org.opencontainers.image.source="https://github.com/9001/copyparty" \
|
||||
org.opencontainers.image.licenses="MIT" \
|
||||
org.opencontainers.image.title="copyparty-iv" \
|
||||
org.opencontainers.image.description="copyparty with Pillow, FFmpeg, libvips (image/audio/video thumbnails, audio transcoding, media tags)"
|
||||
|
@ -28,6 +28,6 @@ COPY i/dist/copyparty-sfx.py innvikler.sh ./
|
|||
ADD base ./base
|
||||
RUN ash innvikler.sh iv
|
||||
|
||||
WORKDIR /w
|
||||
WORKDIR /state
|
||||
EXPOSE 3923
|
||||
ENTRYPOINT ["python3", "-m", "copyparty", "--no-crt", "-c", "/z/initcfg"]
|
||||
ENTRYPOINT ["python3", "-m", "copyparty", "-c", "/z/initcfg"]
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
FROM alpine:latest
|
||||
WORKDIR /z
|
||||
LABEL org.opencontainers.image.url="https://github.com/9001/copyparty" \
|
||||
org.opencontainers.image.source="https://github.com/9001/copyparty/tree/hovudstraum/scripts/docker" \
|
||||
org.opencontainers.image.source="https://github.com/9001/copyparty" \
|
||||
org.opencontainers.image.licenses="MIT" \
|
||||
org.opencontainers.image.title="copyparty-min" \
|
||||
org.opencontainers.image.description="just copyparty, no thumbnails / media tags / audio transcoding"
|
||||
|
@ -13,6 +13,6 @@ RUN apk --no-cache add !pyc \
|
|||
COPY i/dist/copyparty-sfx.py innvikler.sh ./
|
||||
RUN ash innvikler.sh min
|
||||
|
||||
WORKDIR /w
|
||||
WORKDIR /state
|
||||
EXPOSE 3923
|
||||
ENTRYPOINT ["python3", "-m", "copyparty", "--no-crt", "--no-thumb", "-c", "/z/initcfg"]
|
||||
ENTRYPOINT ["python3", "-m", "copyparty", "--no-thumb", "-c", "/z/initcfg"]
|
||||
|
|
|
@ -1,6 +1,19 @@
|
|||
#!/bin/ash
|
||||
set -ex
|
||||
|
||||
tmv() {
|
||||
touch -r "$1" t
|
||||
mv t "$1"
|
||||
}
|
||||
iawk() {
|
||||
awk "$1" <"$2" >t
|
||||
tmv "$2"
|
||||
}
|
||||
ised() {
|
||||
sed -r "$1" <"$2" >t
|
||||
tmv "$2"
|
||||
}
|
||||
|
||||
# use zlib-ng if available
|
||||
f=/z/base/zlib_ng-0.5.1-cp312-cp312-linux_$(cat /etc/apk/arch).whl
|
||||
[ "$1" != min ] && [ -e $f ] && {
|
||||
|
@ -15,9 +28,15 @@ rm -rf /z/base
|
|||
rm -rf /var/cache/apk/* /root/.cache
|
||||
|
||||
# initial config; common for all flavors
|
||||
mkdir /cfg /w
|
||||
chmod 777 /cfg /w
|
||||
echo % /cfg > initcfg
|
||||
mkdir /state /cfg /w
|
||||
chmod 777 /state /cfg /w
|
||||
cat >initcfg <<'EOF'
|
||||
[global]
|
||||
chdir: /w
|
||||
no-crt
|
||||
|
||||
% /cfg
|
||||
EOF
|
||||
|
||||
# unpack sfx and dive in
|
||||
python3 copyparty-sfx.py --version
|
||||
|
@ -32,8 +51,13 @@ rm -rf \
|
|||
/tmp/pe-* /z/copyparty-sfx.py \
|
||||
ensurepip pydoc_data turtle.py turtledemo lib2to3
|
||||
|
||||
cd /usr/lib/python3.*/site-packages/copyparty/
|
||||
rm stolen/surrogateescape.py
|
||||
iawk '/^[^ ]/{s=0}/^if not VENDORED:/{s=1}!s' qrkode.py
|
||||
iawk '/^[^ ]/{s=0}/^ DNS_VND = False/{s=1;print" raise"}!s' mdns.py
|
||||
|
||||
# speedhack
|
||||
sed -ri 's/os.environ.get\("PRTY_NO_IMPRESO"\)/"1"/' /usr/lib/python3.*/site-packages/copyparty/util.py
|
||||
ised 's/os.environ.get\("PRTY_NO_IMPRESO"\)/"1"/' util.py
|
||||
|
||||
# drop bytecode
|
||||
find / -xdev -name __pycache__ -print0 | xargs -0 rm -rf
|
||||
|
@ -46,6 +70,12 @@ find -name __pycache__ |
|
|||
grep -E 'ty/web/|/pycpar' |
|
||||
tr '\n' '\0' | xargs -0 rm -rf
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
smoketest() {
|
||||
|
||||
# two-for-one:
|
||||
# 1) smoketest copyparty even starts
|
||||
# 2) build any bytecode we missed
|
||||
|
@ -82,5 +112,95 @@ kill $pid; wait $pid
|
|||
# output from -e2d
|
||||
rm -rf .hist /cfg/copyparty
|
||||
|
||||
}
|
||||
|
||||
smoketest
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
[ "$1" == min ] && {
|
||||
# shrink amd64 from 45.5 to 33.2 MiB
|
||||
|
||||
# libstdc++ is pulled in by libmpdec++ in libmpdec; keep libmpdec.so
|
||||
cd /usr/lib ; rm -rf \
|
||||
libmpdec++.so* \
|
||||
libncurses* \
|
||||
libpanelw* \
|
||||
libreadline* \
|
||||
libstdc++.so* \
|
||||
--
|
||||
|
||||
cd /usr/lib/python3.*/lib-dynload/ ; rm -rf \
|
||||
*audioop.* \
|
||||
_asyncio.* \
|
||||
_ctypes_test.* \
|
||||
_curses* \
|
||||
_test* \
|
||||
_xx* \
|
||||
ossaudio.* \
|
||||
readline.* \
|
||||
xx* \
|
||||
--
|
||||
|
||||
# keep http/client for u2c
|
||||
cd /usr/lib/python3.*/ ; rm -rf \
|
||||
site-packages/*.dist-info \
|
||||
aifc.py \
|
||||
asyncio \
|
||||
bdb.py \
|
||||
cgi.py \
|
||||
config-3.*/Makefile \
|
||||
ctypes/macholib \
|
||||
dbm \
|
||||
difflib.py \
|
||||
doctest.py \
|
||||
email/_header_value_parser.py \
|
||||
html \
|
||||
http/cookiejar.* \
|
||||
http/server.* \
|
||||
imaplib.py \
|
||||
importlib/resources \
|
||||
mailbox.py \
|
||||
nntplib.py \
|
||||
pickletools.py \
|
||||
pydoc.py \
|
||||
smtplib.py \
|
||||
statistics.py \
|
||||
tomllib \
|
||||
unittest \
|
||||
urllib/request.* \
|
||||
venv \
|
||||
wsgiref \
|
||||
xml/dom \
|
||||
xml/sax \
|
||||
xmlrpc \
|
||||
--
|
||||
|
||||
set +x
|
||||
find -iname '*.pyc' |
|
||||
grep -viE 'tftpy' |
|
||||
while IFS= read -r x; do
|
||||
y="$(printf '%s\n' "$x" | sed -r 's`/__pycache__/([^/]+)\.cpython-312\.pyc$`/\1.py`')"
|
||||
[ -e "$y" ] || continue
|
||||
[ "$y" = "$x" ] && continue
|
||||
rm "$y"
|
||||
mv "$x" "${y}c"
|
||||
done
|
||||
find -iname __pycache__ -print0 | xargs -0 rm -rf --
|
||||
rm -rf /a
|
||||
set -x
|
||||
|
||||
smoketest
|
||||
|
||||
# printf '%s\n' 'FROM localhost/copyparty-min-amd64' 'COPY a /' 'RUN /bin/ash /a' >Dockerfile
|
||||
# podman rmi localhost/m2 ; podman build --squash-all -t m2 . && podman images && podman run --rm -it localhost/m2 --exit=idx && podman images
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# goodbye
|
||||
exec rm innvikler.sh
|
||||
|
|
|
@ -18,6 +18,13 @@ ngs=(
|
|||
dj-{ppc64le,s390x,arm}
|
||||
)
|
||||
|
||||
err=
|
||||
for x in awk jq podman python3 tar wget ; do
|
||||
command -v $x >/dev/null && continue
|
||||
err=1; echo ERROR: missing dependency: $x
|
||||
done
|
||||
[ $err ] && exit 1
|
||||
|
||||
for v in "$@"; do
|
||||
[ "$v" = clean ] && clean=1
|
||||
[ "$v" = hclean ] && hclean=1
|
||||
|
@ -56,7 +63,7 @@ filt=
|
|||
for a in $sarchs; do # arm/v6
|
||||
podman pull --arch=$a alpine:latest
|
||||
done
|
||||
|
||||
|
||||
podman images --format "{{.ID}} {{.History}}" |
|
||||
awk '/library\/alpine/{print$1}' |
|
||||
while read id; do
|
||||
|
@ -102,12 +109,18 @@ filt=
|
|||
# arm takes forever so make it top priority
|
||||
[ ${a::3} == arm ] && nice= || nice=-n20
|
||||
|
||||
# not sure if this is necessary or if inherit-annotations=false was enough, but won't hurt
|
||||
readarray -t annot < <(awk <Dockerfile.$i '/org.opencontainers.image/{sub(/[^\.]+/,"");sub(/[" \\]+$/,"");sub(/"/,"");print"--annotation";print"org"$0}')
|
||||
annot+=( --annotation "org.opencontainers.image.created=$( date -u +%Y-%m-%dT%H:%M:%SZ )" )
|
||||
|
||||
# --pull=never does nothing at all btw
|
||||
(set -x
|
||||
nice $nice podman build \
|
||||
--squash \
|
||||
--pull=never \
|
||||
--from localhost/alpine-$a \
|
||||
--inherit-annotations=false \
|
||||
"${annot[@]}" \
|
||||
-t copyparty-$i-$a$suf \
|
||||
-f Dockerfile.$i . ||
|
||||
(echo $? $i-$a >> err; printf '%096d\n' $(seq 1 42))
|
||||
|
|
|
@ -16,6 +16,8 @@ lics = [
|
|||
"MIT License",
|
||||
"BSD 2-Clause License",
|
||||
"BSD 3-Clause License",
|
||||
"ISC License",
|
||||
"Apache License v2.0",
|
||||
"SIL Open Font License v1.1",
|
||||
]
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue