Compare commits

...

48 Commits

Author SHA1 Message Date
ed
4ccfeeb2cd v1.7.2 2023-05-13 00:00:07 +00:00
ed
2646f6a4f2 oh nice, looks like 3.18 fixed whatever broke in 3.17 2023-05-12 23:38:10 +00:00
ed
b286ab539e readme: add more examples 2023-05-12 22:41:06 +00:00
ed
2cca6e0922 warn when sharing certain system locations 2023-05-12 21:38:16 +00:00
ed
db51f1b063 cfg: allow trailing colon on category headers 2023-05-12 21:01:34 +00:00
ed
d979c47f50 optimize clearTimeout + always shrink upload panes after completion + fix GET alignment 2023-05-12 20:46:45 +00:00
ed
e64b87b99b dont hardlink symlinks (they could be relative) 2023-05-12 20:41:09 +00:00
ed
b985011a00 upgrade docker to alpine 3.18:
* enables chiptune player
* smaller containers (generate pycache at runtime)
2023-05-11 06:56:21 +00:00
ed
c2ed2314c8 pkg/arch: add setuptools 2023-05-08 22:24:46 +00:00
ed
cd496658c3 update pkgs to 1.7.1 2023-05-07 19:51:59 +00:00
ed
deca082623 v1.7.1 2023-05-07 18:34:39 +00:00
ed
0ea8bb7c83 forgot the u2c symlink + sfx listing 2023-05-07 15:45:20 +00:00
ed
1fb251a4c2 was moved to pyproject 2023-05-07 15:41:00 +00:00
ed
4295923b76 rename up2k.py (client) to u2c.py 2023-05-07 15:37:52 +00:00
ed
572aa4b26c rename up2k.py (client) to u2c.py 2023-05-07 15:35:56 +00:00
ed
b1359f039f linter cleanup 2023-05-07 14:38:30 +00:00
ed
867d8ee49e replace setup.py with pyproject.toml + misc cleanup 2023-05-07 14:37:57 +00:00
ed
04c86e8a89 webdav: support write-only folders + force auth option 2023-05-06 20:33:29 +00:00
ed
bc0cb43ef9 include usernames in request logs 2023-05-06 20:17:56 +00:00
ed
769454fdce ftpd: only log invalid passwords 2023-05-06 19:16:52 +00:00
ed
4ee81af8f6 support ';' in passwords 2023-05-06 18:54:55 +00:00
ed
8b0e66122f smoother playback cursor on short songs + optimize 2023-05-06 16:31:04 +00:00
ed
8a98efb929 adapt to new archpkg layout 2023-05-05 20:51:18 +00:00
ed
b6fd555038 panic if two accounts have the same password 2023-05-05 20:24:24 +00:00
ed
7eb413ad51 doc tweaks 2023-05-05 19:39:10 +00:00
ixces
4421d509eb update PKGBUILD 2023-05-02 17:21:12 +02:00
ed
793ffd7b01 update pkgs to 1.7.0 2023-04-29 22:50:36 +00:00
ed
1e22222c60 v1.7.0 2023-04-29 21:14:38 +00:00
ed
544e0549bc make xvol and xdev apply at runtime (closes #24):
* when accessing files inside an xdev volume, verify that the file
   exists on the same device/filesystem as the volume root

* when accessing files inside an xvol volume, verify that the file
   exists within any volume where the user has read access
2023-04-29 21:10:02 +00:00
ed
83178d0836 preserve empty folders (closes #23):
* when deleting files, do not cascade upwards through empty folders
* when moving folders, also move any empty folders inside

the only remaining action which autoremoves empty folders is
files getting deleted as they expire volume lifetimes

also prevents accidentally moving parent folders into subfolders
(even though that actually worked surprisingly well)
2023-04-29 11:30:43 +00:00
ed
c44f5f5701 nit 2023-04-29 09:44:46 +00:00
ed
138f5bc989 warn about android powersave settings on music interruption + fix eq on folder change 2023-04-29 09:31:53 +00:00
ed
e4759f86ef ftpd correctness:
* winscp mkdir failed because the folder-not-found error got repeated
* rmdir fails after all files in the folder have poofed; that's OK
* add --ftp4 as a precaution
2023-04-28 20:50:45 +00:00
ed
d71416437a show file selection summary 2023-04-27 19:33:52 +00:00
ed
a84c583b2c ok that wasn't enough 2023-04-27 19:06:35 +00:00
ed
cdacdccdb8 update pkgs to 1.6.15 2023-04-27 00:36:56 +00:00
ed
d3ccd3f174 v1.6.15 2023-04-26 23:00:55 +00:00
ed
cb6de0387d a bit faster 2023-04-26 19:56:27 +00:00
ed
abff40519d eyecandy: restore playback indicator on folder hop 2023-04-26 19:09:16 +00:00
ed
55c74ad164 30% faster folder listings (wtf...) 2023-04-26 18:55:53 +00:00
ed
673b4f7e23 option to show symlink's lastmod instead of deref;
mainly motivated by u2cli's folder syncing in turbo mode
which would un-turbo on most dupes due to wrong lastmod

disabled by default for regular http listings
(to avoid confusion in most regular usecases),
enable per-request with urlparam lt

enabled by default for single-level webdav listings
(because rclone hits the same issue as u2cli),
can be disabled with arg --dav-rt or volflag davrt

impossible to enable for recursive webdav listings
2023-04-26 18:54:21 +00:00
ed
d11e02da49 u2cli: avoid dns lookups while uploading 2023-04-26 18:46:42 +00:00
ed
8790f89e08 fix installing from source tarball 2023-04-26 18:40:47 +00:00
ed
33442026b8 try to discourage android from stopping playback...
...when continuing into the next folder

accidentally introduces a neat bonus feature where the music
no longer stops while you go looking for stuff to play next
2023-04-26 18:33:30 +00:00
ed
03193de6d0 socket read/write timeout 2023-04-24 20:04:22 +00:00
ed
8675ff40f3 update pkgs to 1.6.14 2023-04-24 07:52:12 +00:00
ed
d88889d3fc v1.6.14 2023-04-24 06:09:44 +00:00
ed
6f244d4335 update pkgs to 1.6.13 2023-04-24 00:46:47 +00:00
57 changed files with 1076 additions and 367 deletions

View File

@@ -1,2 +1,2 @@
Please include the following text somewhere in this PR description:
To show that your contribution is compatible with the MIT License, please include the following text somewhere in this PR description:
This PR complies with the DCO; https://developercertificate.org/

1
.gitignore vendored
View File

@@ -37,6 +37,7 @@ up.*.txt
.hist/
scripts/docker/*.out
scripts/docker/*.err
/perf.*
# nix build output link
result

10
.vscode/launch.py vendored
View File

@@ -30,9 +30,17 @@ except:
argv = [os.path.expanduser(x) if x.startswith("~") else x for x in argv]
sfx = ""
if len(sys.argv) > 1 and os.path.isfile(sys.argv[1]):
sfx = sys.argv[1]
sys.argv = [sys.argv[0]] + sys.argv[2:]
argv += sys.argv[1:]
if re.search(" -j ?[0-9]", " ".join(argv)):
if sfx:
argv = [sys.executable, sfx] + argv
sp.check_call(argv)
elif re.search(" -j ?[0-9]", " ".join(argv)):
argv = [sys.executable, "-m", "copyparty"] + argv
sp.check_call(argv)
else:

32
.vscode/settings.json vendored
View File

@@ -35,34 +35,18 @@
"python.linting.flake8Enabled": true,
"python.linting.banditEnabled": true,
"python.linting.mypyEnabled": true,
"python.linting.mypyArgs": [
"--ignore-missing-imports",
"--follow-imports=silent",
"--show-column-numbers",
"--strict"
],
"python.linting.flake8Args": [
"--max-line-length=120",
"--ignore=E722,F405,E203,W503,W293,E402,E501,E128",
"--ignore=E722,F405,E203,W503,W293,E402,E501,E128,E226",
],
"python.linting.banditArgs": [
"--ignore=B104"
],
"python.linting.pylintArgs": [
"--disable=missing-module-docstring",
"--disable=missing-class-docstring",
"--disable=missing-function-docstring",
"--disable=import-outside-toplevel",
"--disable=wrong-import-position",
"--disable=raise-missing-from",
"--disable=bare-except",
"--disable=broad-except",
"--disable=invalid-name",
"--disable=line-too-long",
"--disable=consider-using-f-string"
"--ignore=B104,B110,B112"
],
// python3 -m isort --py=27 --profile=black copyparty/
"python.formatting.provider": "black",
"python.formatting.provider": "none",
"[python]": {
"editor.defaultFormatter": "ms-python.black-formatter"
},
"editor.formatOnSave": true,
"[html]": {
"editor.formatOnSave": false,
@@ -74,10 +58,6 @@
"files.associations": {
"*.makefile": "makefile"
},
"python.formatting.blackArgs": [
"-t",
"py27"
],
"python.linting.enabled": true,
"python.pythonPath": "/usr/bin/python3"
}

View File

@@ -41,6 +41,7 @@ turn almost any device into a file server with resumable uploads/downloads using
* [batch rename](#batch-rename) - select some files and press `F2` to bring up the rename UI
* [media player](#media-player) - plays almost every audio format there is
* [audio equalizer](#audio-equalizer) - bass boosted
* [fix unreliable playback on android](#fix-unreliable-playback-on-android) - due to phone / app settings
* [markdown viewer](#markdown-viewer) - and there are *two* editors
* [other tricks](#other-tricks)
* [searching](#searching) - search by size, date, path/name, mp3-tags, ...
@@ -69,6 +70,8 @@ turn almost any device into a file server with resumable uploads/downloads using
* [themes](#themes)
* [complete examples](#complete-examples)
* [reverse-proxy](#reverse-proxy) - running copyparty next to other websites
* [packages](#packages) - the party might be closer than you think
* [arch package](#arch-package) - now [available on aur](https://aur.archlinux.org/packages/copyparty) maintained by [@icxes](https://github.com/icxes)
* [nix package](#nix-package) - `nix profile install github:9001/copyparty`
* [nixos module](#nixos-module)
* [browser support](#browser-support) - TLDR: yes
@@ -101,9 +104,9 @@ turn almost any device into a file server with resumable uploads/downloads using
just run **[copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py)** -- that's it! 🎉
* or install through pypi (python3 only): `python3 -m pip install --user -U copyparty`
* or install through pypi: `python3 -m pip install --user -U copyparty`
* or if you cannot install python, you can use [copyparty.exe](#copypartyexe) instead
* or [install through nix](#nix-package), or [on NixOS](#nixos-module)
* or install [on arch](#arch-package) [on NixOS](#nixos-module) [through nix](#nix-package)
* or if you are on android, [install copyparty in termux](#install-on-android)
* or if you prefer to [use docker](./scripts/docker/) 🐋 you can do that too
* docker has all deps built-in, so skip this step:
@@ -123,7 +126,7 @@ enable thumbnails (images/audio/video), media indexing, and audio transcoding by
running copyparty without arguments (for example doubleclicking it on Windows) will give everyone read/write access to the current folder; you may want [accounts and volumes](#accounts-and-volumes)
or see [complete windows example](./docs/examples/windows.md)
or see [some usage examples](#complete-examples) for inspiration, or the [complete windows example](./docs/examples/windows.md)
some recommended options:
* `-e2dsa` enables general [file indexing](#file-indexing)
@@ -275,6 +278,8 @@ server notes:
* [Firefox issue 1790500](https://bugzilla.mozilla.org/show_bug.cgi?id=1790500) -- entire browser can crash after uploading ~4000 small files
* Android: music playback randomly stops due to [battery usage settings](#fix-unreliable-playback-on-android)
* iPhones: the volume control doesn't work because [apple doesn't want it to](https://developer.apple.com/library/archive/documentation/AudioVideo/Conceptual/Using_HTML5_Audio_Video/Device-SpecificConsiderations/Device-SpecificConsiderations.html#//apple_ref/doc/uid/TP40009523-CH5-SW11)
* *future workaround:* enable the equalizer, make it all-zero, and set a negative boost to reduce the volume
* "future" because `AudioContext` can't maintain a stable playback speed in the current iOS version (15.7), maybe one day...
@@ -300,7 +305,7 @@ upgrade notes
* http-api: delete/move is now `POST` instead of `GET`
* everything other than `GET` and `HEAD` must pass [cors validation](#cors)
* `1.5.0` (2022-12-03): [new chunksize formula](https://github.com/9001/copyparty/commit/54e1c8d261df) for files larger than 128 GiB
* **users:** upgrade to the latest [cli uploader](https://github.com/9001/copyparty/blob/hovudstraum/bin/up2k.py) if you use that
* **users:** upgrade to the latest [cli uploader](https://github.com/9001/copyparty/blob/hovudstraum/bin/u2c.py) if you use that
* **devs:** update third-party up2k clients (if those even exist)
@@ -507,7 +512,7 @@ you can also zip a selection of files or folders by clicking them in the browser
## uploading
drag files/folders into the web-browser to upload (or use the [command-line uploader](https://github.com/9001/copyparty/tree/hovudstraum/bin#up2kpy))
drag files/folders into the web-browser to upload (or use the [command-line uploader](https://github.com/9001/copyparty/tree/hovudstraum/bin#u2cpy))
this initiates an upload using `up2k`; there are two uploaders available:
* `[🎈] bup`, the basic uploader, supports almost every browser since netscape 4.0
@@ -701,6 +706,11 @@ can also boost the volume in general, or increase/decrease stereo width (like [c
has the convenient side-effect of reducing the pause between songs, so gapless albums play better with the eq enabled (just make it flat)
### fix unreliable playback on android
due to phone / app settings, android phones may randomly stop playing music when the power saver kicks in, especially at the end of an album -- you can fix it by [disabling power saving](https://user-images.githubusercontent.com/241032/235262123-c328cca9-3930-4948-bd18-3949b9fd3fcf.png) in the [app settings](https://user-images.githubusercontent.com/241032/235262121-2ffc51ae-7821-4310-a322-c3b7a507890c.png) of the browser you use for music streaming (preferably a dedicated one)
## markdown viewer
and there are *two* editors
@@ -948,7 +958,11 @@ avoid traversing into other filesystems using `--xdev` / volflag `:c,xdev`, ski
and/or you can `--xvol` / `:c,xvol` to ignore all symlinks leaving the volume's top directory, but still allow bind-mounts pointing elsewhere
**NB: only affects the indexer** -- users can still access anything inside a volume, unless shadowed by another volume
* symlinks are permitted with `xvol` if they point into another volume where the user has the same level of access
these options will reduce performance; unlikely worst-case estimates are 14% reduction for directory listings, 35% for download-as-tar
as of copyparty v1.7.0 these options also prevent file access at runtime -- in previous versions it was just hints for the indexer
### periodic rescan
@@ -1144,9 +1158,33 @@ see the top of [./copyparty/web/browser.css](./copyparty/web/browser.css) where
## complete examples
* [running on windows](./docs/examples/windows.md)
* see [running on windows](./docs/examples/windows.md) for a fancy windows setup
* read-only music server
* or use any of the examples below, just replace `python copyparty-sfx.py` with `copyparty.exe` if you're using the exe edition
* allow anyone to download or upload files into the current folder:
`python copyparty-sfx.py`
* enable searching and music indexing with `-e2dsa -e2ts`
* start an FTP server on port 3921 with `--ftp 3921`
* announce it on your LAN with `-z` so it appears in windows/Linux file managers
* anyone can upload, but nobody can see any files (even the uploader):
`python copyparty-sfx.py -e2dsa -v .::w`
* block uploads if there's less than 4 GiB free disk space with `--df 4`
* show a popup on new uploads with `--xau bin/hooks/notify.py`
* anyone can upload, and receive "secret" links for each upload they do:
`python copyparty-sfx.py -e2dsa -v .::wG:c,fk=8`
* anyone can browse, only `kevin` (password `okgo`) can upload/move/delete files:
`python copyparty-sfx.py -e2dsa -a kevin:okgo -v .::r:rwmd,kevin`
* read-only music server:
`python copyparty-sfx.py -v /mnt/nas/music:/music:r -e2dsa -e2ts --no-robots --force-js --theme 2`
* ...with bpm and key scanning
@@ -1176,6 +1214,16 @@ example webserver configs:
* [apache2 config](contrib/apache/copyparty.conf) -- location-based
# packages
the party might be closer than you think
## arch package
now [available on aur](https://aur.archlinux.org/packages/copyparty) maintained by [@icxes](https://github.com/icxes)
## nix package
`nix profile install github:9001/copyparty`
@@ -1350,10 +1398,10 @@ interact with copyparty using non-browser clients
* `(printf 'PUT /junk?pw=wark HTTP/1.1\r\n\r\n'; cat movie.mkv) | nc 127.0.0.1 3923`
* `(printf 'PUT / HTTP/1.1\r\n\r\n'; cat movie.mkv) >/dev/tcp/127.0.0.1/3923`
* python: [up2k.py](https://github.com/9001/copyparty/blob/hovudstraum/bin/up2k.py) is a command-line up2k client [(webm)](https://ocv.me/stuff/u2cli.webm)
* python: [u2c.py](https://github.com/9001/copyparty/blob/hovudstraum/bin/u2c.py) is a command-line up2k client [(webm)](https://ocv.me/stuff/u2cli.webm)
* file uploads, file-search, [folder sync](#folder-sync), autoresume of aborted/broken uploads
* can be downloaded from copyparty: controlpanel -> connect -> [up2k.py](http://127.0.0.1:3923/.cpr/a/up2k.py)
* see [./bin/README.md#up2kpy](bin/README.md#up2kpy)
* can be downloaded from copyparty: controlpanel -> connect -> [u2c.py](http://127.0.0.1:3923/.cpr/a/u2c.py)
* see [./bin/README.md#u2cpy](bin/README.md#u2cpy)
* FUSE: mount a copyparty server as a local filesystem
* cross-platform python client available in [./bin/](bin/)
@@ -1376,11 +1424,11 @@ NOTE: curl will not send the original filename if you use `-T` combined with url
sync folders to/from copyparty
the commandline uploader [up2k.py](https://github.com/9001/copyparty/tree/hovudstraum/bin#up2kpy) 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)
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)
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 (currently [in beta](https://beta.rclone.org/?filter=latest)), rclone will also be faster than up2k.py
* starting from rclone v1.63 (currently [in beta](https://beta.rclone.org/?filter=latest)), rclone will also be faster than u2c.py
## mount as drive
@@ -1447,7 +1495,7 @@ when uploading files,
* chrome is recommended, at least compared to firefox:
* up to 90% faster when hashing, especially on SSDs
* up to 40% faster when uploading over extremely fast internets
* but [up2k.py](https://github.com/9001/copyparty/blob/hovudstraum/bin/up2k.py) can be 40% faster than chrome again
* but [u2c.py](https://github.com/9001/copyparty/blob/hovudstraum/bin/u2c.py) can be 40% faster than chrome again
* if you're cpu-bottlenecked, or the browser is maxing a cpu core:
* up to 30% faster uploads if you hide the upload status list by switching away from the `[🚀]` up2k ui-tab (or closing it)

View File

@@ -1,4 +1,4 @@
# [`up2k.py`](up2k.py)
# [`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

View File

@@ -1,13 +1,13 @@
#!/usr/bin/env python3
from __future__ import print_function, unicode_literals
S_VERSION = "1.6"
S_BUILD_DT = "2023-04-20"
S_VERSION = "1.9"
S_BUILD_DT = "2023-05-07"
"""
up2k.py: upload to copyparty
u2c.py: upload to copyparty
2021, ed <irc.rizon.net>, MIT-Licensed
https://github.com/9001/copyparty/blob/hovudstraum/bin/up2k.py
https://github.com/9001/copyparty/blob/hovudstraum/bin/u2c.py
- dependencies: requests
- supports python 2.6, 2.7, and 3.3 through 3.12
@@ -21,6 +21,7 @@ import math
import time
import atexit
import signal
import socket
import base64
import hashlib
import platform
@@ -58,6 +59,7 @@ PY2 = sys.version_info < (3,)
if PY2:
from Queue import Queue
from urllib import quote, unquote
from urlparse import urlsplit, urlunsplit
sys.dont_write_bytecode = True
bytes = str
@@ -65,6 +67,7 @@ else:
from queue import Queue
from urllib.parse import unquote_to_bytes as unquote
from urllib.parse import quote_from_bytes as quote
from urllib.parse import urlsplit, urlunsplit
unicode = str
@@ -337,6 +340,32 @@ class CTermsize(object):
ss = CTermsize()
def undns(url):
usp = urlsplit(url)
hn = usp.hostname
gai = None
eprint("resolving host [{0}] ...".format(hn), end="")
try:
gai = socket.getaddrinfo(hn, None)
hn = gai[0][4][0]
except KeyboardInterrupt:
raise
except:
t = "\n\033[31mfailed to resolve upload destination host;\033[0m\ngai={0}\n"
eprint(t.format(repr(gai)))
raise
if usp.port:
hn = "{0}:{1}".format(hn, usp.port)
if usp.username or usp.password:
hn = "{0}:{1}@{2}".format(usp.username, usp.password, hn)
usp = usp._replace(netloc=hn)
url = urlunsplit(usp)
eprint(" {0}".format(url))
return url
def _scd(err, top):
"""non-recursive listing of directory contents, along with stat() info"""
with os.scandir(top) as dh:
@@ -853,7 +882,7 @@ class Ctl(object):
print(" ls ~{0}".format(srd))
zb = self.ar.url.encode("utf-8")
zb += quotep(rd.replace(b"\\", b"/"))
r = req_ses.get(zb + b"?ls&dots", headers=headers)
r = req_ses.get(zb + b"?ls&lt&dots", headers=headers)
if not r:
raise Exception("HTTP {0}".format(r.status_code))
@@ -931,7 +960,7 @@ class Ctl(object):
upath = file.abs.decode("utf-8", "replace")
if not VT100:
upath = upath[4:]
upath = upath.lstrip("\\?")
hs, sprs = handshake(self.ar, file, search)
if search:
@@ -1073,6 +1102,7 @@ source file/folder selection uses rsync syntax, meaning that:
ap = app.add_argument_group("compatibility")
ap.add_argument("--cls", action="store_true", help="clear screen before start")
ap.add_argument("--rh", type=int, metavar="TRIES", default=0, help="resolve server hostname before upload (good for buggy networks, but TLS certs will break)")
ap = app.add_argument_group("folder sync")
ap.add_argument("--dl", action="store_true", help="delete local files after uploading")
@@ -1096,7 +1126,7 @@ source file/folder selection uses rsync syntax, meaning that:
ar = app.parse_args()
finally:
if EXE and not sys.argv[1:]:
print("*** hit enter to exit ***")
eprint("*** hit enter to exit ***")
try:
input()
except:
@@ -1129,8 +1159,18 @@ source file/folder selection uses rsync syntax, meaning that:
with open(fn, "rb") as f:
ar.a = f.read().decode("utf-8").strip()
for n in range(ar.rh):
try:
ar.url = undns(ar.url)
break
except KeyboardInterrupt:
raise
except:
if n > ar.rh - 2:
raise
if ar.cls:
print("\x1b\x5b\x48\x1b\x5b\x32\x4a\x1b\x5b\x33\x4a", end="")
eprint("\x1b\x5b\x48\x1b\x5b\x32\x4a\x1b\x5b\x33\x4a", end="")
ctl = Ctl(ar)

View File

@@ -1,14 +1,14 @@
# Maintainer: icxes <dev.null@need.moe>
pkgname=copyparty
pkgver="1.6.12"
pkgver="1.7.1"
pkgrel=1
pkgdesc="Portable file sharing hub"
arch=("any")
url="https://github.com/9001/${pkgname}"
license=('MIT')
depends=("python" "lsof")
optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tags"
"python-jinja: faster html generator"
depends=("python" "lsof" "python-jinja")
makedepends=("python-wheel" "python-setuptools" "python-build" "python-installer" "make" "pigz")
optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tags"
"python-mutagen: music tags (alternative)"
"python-pillow: thumbnails for images"
"python-pyvips: thumbnails for images (higher quality, faster, uses more ram)"
@@ -17,34 +17,31 @@ optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tag
"python-pyopenssl: ftps functionality"
"python-impacket-git: smb support (bad idea)"
)
source=("${url}/releases/download/v${pkgver}/${pkgname}-sfx.py"
"${pkgname}.conf"
"${pkgname}.service"
"prisonparty.service"
"index.md"
"https://raw.githubusercontent.com/9001/${pkgname}/v${pkgver}/bin/prisonparty.sh"
"https://raw.githubusercontent.com/9001/${pkgname}/v${pkgver}/LICENSE"
)
source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz")
backup=("etc/${pkgname}.d/init" )
sha256sums=("ce3702634fecf7cd6a5c79709a8e6cb1e9fe0748ec4754a278be7a60b618dd42"
"b8565eba5e64dedba1cf6c7aac7e31c5a731ed7153d6810288a28f00a36c28b2"
"f65c207e0670f9d78ad2e399bda18d5502ff30d2ac79e0e7fc48e7fbdc39afdc"
"c4f396b083c9ec02ad50b52412c84d2a82be7f079b2d016e1c9fad22d68285ff"
"dba701de9fd584405917e923ea1e59dbb249b96ef23bad479cf4e42740b774c8"
"8e89d281483e22d11d111bed540652af35b66af6f14f49faae7b959f6cdc6475"
"cb2ce3d6277bf2f5a82ecf336cc44963bc6490bcf496ffbd75fc9e21abaa75f3"
)
sha256sums=("b369966e65a0c0f622b5dea231e3e62649d3b6ef3074c1b2a88335e48e024604")
build() {
cd "${srcdir}/${pkgname}-${pkgver}"
pushd copyparty/web
make -j$(nproc)
rm Makefile
popd
python3 -m build -wn
}
package() {
cd "${srcdir}/"
cd "${srcdir}/${pkgname}-${pkgver}"
python3 -m installer -d "$pkgdir" dist/*.whl
install -dm755 "${pkgdir}/etc/${pkgname}.d"
install -Dm755 "${pkgname}-sfx.py" "${pkgdir}/usr/bin/${pkgname}"
install -Dm755 "prisonparty.sh" "${pkgdir}/usr/bin/prisonparty"
install -Dm644 "${pkgname}.conf" "${pkgdir}/etc/${pkgname}.d/init"
install -Dm644 "${pkgname}.service" "${pkgdir}/usr/lib/systemd/system/${pkgname}.service"
install -Dm644 "prisonparty.service" "${pkgdir}/usr/lib/systemd/system/prisonparty.service"
install -Dm644 "index.md" "${pkgdir}/var/lib/${pkgname}-jail/README.md"
install -Dm755 "bin/prisonparty.sh" "${pkgdir}/usr/bin/prisonparty"
install -Dm644 "contrib/package/arch/${pkgname}.conf" "${pkgdir}/etc/${pkgname}.d/init"
install -Dm644 "contrib/package/arch/${pkgname}.service" "${pkgdir}/usr/lib/systemd/system/${pkgname}.service"
install -Dm644 "contrib/package/arch/prisonparty.service" "${pkgdir}/usr/lib/systemd/system/prisonparty.service"
install -Dm644 "contrib/package/arch/index.md" "${pkgdir}/var/lib/${pkgname}-jail/README.md"
install -Dm644 "LICENSE" "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE"
find /etc/${pkgname}.d -iname '*.conf' 2>/dev/null | grep -qE . && return

View File

@@ -1,5 +1,5 @@
{
"url": "https://github.com/9001/copyparty/releases/download/v1.6.12/copyparty-sfx.py",
"version": "1.6.12",
"hash": "sha256-zjcCY0/s981qXHlwmo5ssen+B0jsR1SieL56YLYY3UI="
"url": "https://github.com/9001/copyparty/releases/download/v1.7.1/copyparty-sfx.py",
"version": "1.7.1",
"hash": "sha256-Mg3z5DkOhQc58bbjkEKo/UiLehghIYjR66LcYHqlm1o="
}

View File

@@ -186,7 +186,7 @@ def init_E(E: EnvParams) -> None:
with open_binary("copyparty", "z.tar") as tgz:
with tarfile.open(fileobj=tgz) as tf:
tf.extractall(tdn)
tf.extractall(tdn) # nosec (archive is safe)
return tdn
@@ -201,7 +201,7 @@ def init_E(E: EnvParams) -> None:
E.mod = _unpack()
if sys.platform == "win32":
bdir = os.environ.get("APPDATA") or os.environ.get("TEMP")
bdir = os.environ.get("APPDATA") or os.environ.get("TEMP") or "."
E.cfg = os.path.normpath(bdir + "/copyparty")
elif sys.platform == "darwin":
E.cfg = os.path.expanduser("~/Library/Preferences/copyparty")
@@ -279,8 +279,8 @@ def ensure_cert(al: argparse.Namespace) -> None:
with open(al.cert, "rb") as f:
buf = f.read()
o1 = buf.find(b"-BEGIN PRIVATE KEY-")
o2 = buf.find(b"-BEGIN CERTIFICATE-")
o1 = buf.find(b" PRIVATE KEY-")
o2 = buf.find(b" CERTIFICATE-")
m = "unsupported certificate format: "
if o1 < 0:
raise Exception(m + "no private key inside pem")
@@ -619,9 +619,9 @@ def get_sects():
\033[32macid\033[0m = extremely safe but slow; the old default. Should never lose any data no matter what
\033[32mswal\033[0m = 2.4x faster uploads yet 99.9%% as safe -- theoretical chance of losing metadata for the ~200 most recently uploaded files if there's a power-loss or your OS crashes
\033[32mswal\033[0m = 2.4x faster uploads yet 99.9% as safe -- theoretical chance of losing metadata for the ~200 most recently uploaded files if there's a power-loss or your OS crashes
\033[32mwal\033[0m = another 21x faster on HDDs yet 90%% as safe; same pitfall as \033[33mswal\033[0m except more likely
\033[32mwal\033[0m = another 21x faster on HDDs yet 90% as safe; same pitfall as \033[33mswal\033[0m except more likely
\033[32myolo\033[0m = another 1.5x faster, and removes the occasional sudden upload-pause while the disk syncs, but now you're at risk of losing the entire database in a powerloss / OS-crash
@@ -710,6 +710,8 @@ def add_network(ap):
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")
else:
ap2.add_argument("--freebind", action="store_true", help="allow listening on IPs which do not yet exist, for example if the network interfaces haven't finished going up. Only makes sense for IPs other than '0.0.0.0', '127.0.0.1', '::', and '::1'. May require running as root (unless net.ipv6.ip_nonlocal_bind)")
ap2.add_argument("--s-thead", metavar="SEC", type=int, default=120, help="socket timeout (read request header)")
ap2.add_argument("--s-tbody", metavar="SEC", type=float, default=186, help="socket timeout (read/write request/response bodies). Use 60 on fast servers (default is extremely safe). Disable with 0 if reverse-proxied for a 2%% speed boost")
ap2.add_argument("--s-wr-sz", metavar="B", type=int, default=256*1024, help="socket write size in bytes")
ap2.add_argument("--s-wr-slp", metavar="SEC", type=float, default=0, help="debug: socket write delay in seconds")
ap2.add_argument("--rsp-slp", metavar="SEC", type=float, default=0, help="debug: response delay in seconds")
@@ -771,6 +773,7 @@ def add_ftp(ap):
ap2.add_argument("--ftp", metavar="PORT", type=int, help="enable FTP server on PORT, for example \033[32m3921")
ap2.add_argument("--ftps", metavar="PORT", type=int, help="enable FTPS server on PORT, for example \033[32m3990")
ap2.add_argument("--ftpv", action="store_true", help="verbose")
ap2.add_argument("--ftp4", action="store_true", help="only listen on IPv4")
ap2.add_argument("--ftp-wt", metavar="SEC", type=int, default=7, help="grace period for resuming interrupted uploads (any client can write to any file last-modified more recently than SEC seconds ago)")
ap2.add_argument("--ftp-nat", metavar="ADDR", type=u, help="the NAT address to use for passive connections")
ap2.add_argument("--ftp-pr", metavar="P-P", type=u, help="the range of TCP ports to use for passive connections, for example \033[32m12000-13000")
@@ -781,6 +784,8 @@ def add_webdav(ap):
ap2.add_argument("--daw", action="store_true", help="enable full write support, even if client may not be webdav. \033[1;31mWARNING:\033[0m This has side-effects -- PUT-operations will now \033[1;31mOVERWRITE\033[0m existing files, rather than inventing new filenames to avoid loss of data. You might want to instead set this as a volflag where needed. By not setting this flag, uploaded files can get written to a filename which the client does not expect (which might be okay, depending on client)")
ap2.add_argument("--dav-inf", action="store_true", help="allow depth:infinite requests (recursive file listing); extremely server-heavy but required for spec compliance -- luckily few clients rely on this")
ap2.add_argument("--dav-mac", action="store_true", help="disable apple-garbage filter -- allow macos to create junk files (._* and .DS_Store, .Spotlight-*, .fseventsd, .Trashes, .AppleDouble, __MACOS)")
ap2.add_argument("--dav-rt", action="store_true", help="show symlink-destination's lastmodified instead of the link itself; always enabled for recursive listings (volflag=davrt)")
ap2.add_argument("--dav-auth", action="store_true", help="force auth for all folders (required by davfs2 when only some folders are world-readable) (volflag=davauth)")
def add_smb(ap):
@@ -834,6 +839,8 @@ def add_safety(ap, fk_salt):
ap2.add_argument("-ss", action="store_true", help="further increase safety: Prevent js-injection, accidental move/delete, broken symlinks, webdav, 404 on 403, ban on excessive 404s.\n └─Alias of\033[32m -s --unpost=0 --no-del --no-mv --hardlink --vague-403 --ban-404=50,60,1440 -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, help="do a sanity/safety check of all volumes on startup; arguments \033[33mUSER\033[0m,\033[33mVOL\033[0m,\033[33mFLAGS\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)")
ap2.add_argument("--xdev", action="store_true", help="stay within the filesystem of the volume root; do not descend into other devices (symlink or bind-mount to another HDD, ...) (volflag=xdev)")
ap2.add_argument("--salt", type=u, default="hunter2", help="up2k file-hash salt; serves no purpose, no reason to change this (but delete all databases if you do)")
ap2.add_argument("--fk-salt", metavar="SALT", type=u, default=fk_salt, help="per-file accesskey salt; used to generate unpredictable URLs for hidden files -- this one DOES matter")
ap2.add_argument("--no-dot-mv", action="store_true", help="disallow moving dotfiles; makes it impossible to move folders containing dotfiles")
@@ -927,8 +934,6 @@ def add_db_general(ap, hcores):
ap2.add_argument("--no-forget", action="store_true", help="never forget indexed files, even when deleted from disk -- makes it impossible to ever upload the same file twice (volflag=noforget)")
ap2.add_argument("--dbd", metavar="PROFILE", default="wal", help="database durability profile; sets the tradeoff between robustness and speed, see --help-dbd (volflag=dbd)")
ap2.add_argument("--xlink", action="store_true", help="on upload: check all volumes for dupes, not just the target volume (volflag=xlink)")
ap2.add_argument("--xdev", action="store_true", help="do not descend into other filesystems (symlink or bind-mount to another HDD, ...) (volflag=xdev)")
ap2.add_argument("--xvol", action="store_true", help="skip symlinks leaving the volume root (volflag=xvol)")
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="disk rescan volume interval, 0=off (volflag=scan)")
ap2.add_argument("--db-act", metavar="SEC", type=float, default=10, help="defer any scheduled volume reindexing until SEC seconds after last db write (uploads, renames, ...)")

View File

@@ -1,8 +1,8 @@
# coding: utf-8
VERSION = (1, 6, 13)
CODENAME = "cors k"
BUILD_DT = (2023, 4, 23)
VERSION = (1, 7, 2)
CODENAME = "unlinked"
BUILD_DT = (2023, 5, 13)
S_VERSION = ".".join(map(str, VERSION))
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)

View File

@@ -67,9 +67,9 @@ class AXS(object):
self.upget: set[str] = set(upget or [])
def __repr__(self) -> str:
return "AXS({})".format(
return "AXS(%s)" % (
", ".join(
"{}={!r}".format(k, self.__dict__[k])
"%s=%r" % (k, self.__dict__[k])
for k in "uread uwrite umove udel uget upget".split()
)
)
@@ -285,6 +285,8 @@ class VFS(object):
self.vpath = vpath # absolute path in the virtual filesystem
self.axs = axs
self.flags = flags # config options
self.root = self
self.dev = 0 # st_dev
self.nodes: dict[str, VFS] = {} # child nodes
self.histtab: dict[str, str] = {} # all realpath->histpath
self.dbv: Optional[VFS] = None # closest full/non-jump parent
@@ -297,26 +299,42 @@ class VFS(object):
self.apget: dict[str, list[str]] = {}
if realpath:
rp = realpath + ("" if realpath.endswith(os.sep) else os.sep)
vp = vpath + ("/" if vpath else "")
self.histpath = os.path.join(realpath, ".hist") # db / thumbcache
self.all_vols = {vpath: self} # flattened recursive
self.all_aps = [(rp, self)]
self.all_vps = [(vp, self)]
else:
self.histpath = ""
self.all_vols = {}
self.all_aps = []
self.all_vps = []
def __repr__(self) -> str:
return "VFS({})".format(
return "VFS(%s)" % (
", ".join(
"{}={!r}".format(k, self.__dict__[k])
"%s=%r" % (k, self.__dict__[k])
for k in "realpath vpath axs flags".split()
)
)
def get_all_vols(self, outdict: dict[str, "VFS"]) -> None:
def get_all_vols(
self,
vols: dict[str, "VFS"],
aps: list[tuple[str, "VFS"]],
vps: list[tuple[str, "VFS"]],
) -> None:
if self.realpath:
outdict[self.vpath] = self
vols[self.vpath] = self
rp = self.realpath
rp += "" if rp.endswith(os.sep) else os.sep
vp = self.vpath + ("/" if self.vpath else "")
aps.append((rp, self))
vps.append((vp, self))
for v in self.nodes.values():
v.get_all_vols(outdict)
v.get_all_vols(vols, aps, vps)
def add(self, src: str, dst: str) -> "VFS":
"""get existing, or add new path to the vfs"""
@@ -390,7 +408,11 @@ class VFS(object):
self, vpath: str, uname: str
) -> tuple[bool, bool, bool, bool, bool, bool]:
"""can Read,Write,Move,Delete,Get,Upget"""
vn, _ = self._find(undot(vpath))
if vpath:
vn, _ = self._find(undot(vpath))
else:
vn = self
c = vn.axs
return (
uname in c.uread or "*" in c.uread,
@@ -545,9 +567,20 @@ class VFS(object):
self.log("vfs.walk", t.format(seen[-1], fsroot, self.vpath, rem), 3)
return
if "xdev" in self.flags or "xvol" in self.flags:
rm1 = []
for le in vfs_ls:
ap = absreal(os.path.join(fsroot, le[0]))
vn2 = self.chk_ap(ap)
if not vn2 or not vn2.get("", uname, True, False):
rm1.append(le)
_ = [vfs_ls.remove(x) for x in rm1] # type: ignore
seen = seen[:] + [fsroot]
rfiles = [x for x in vfs_ls if not stat.S_ISDIR(x[1].st_mode)]
rdirs = [x for x in vfs_ls if stat.S_ISDIR(x[1].st_mode)]
# if lstat: ignore folder symlinks since copyparty will never make those
# (and we definitely don't want to descend into them)
rfiles.sort()
rdirs.sort()
@@ -641,6 +674,44 @@ class VFS(object):
for d in [{"vp": v, "ap": a, "st": n} for v, a, n in ret2]:
yield d
def chk_ap(self, ap: str, st: Optional[os.stat_result] = None) -> Optional["VFS"]:
aps = ap + os.sep
if "xdev" in self.flags and not ANYWIN:
if not st:
ap2 = ap.replace("\\", "/") if ANYWIN else ap
while ap2:
try:
st = bos.stat(ap2)
break
except:
if "/" not in ap2:
raise
ap2 = ap2.rsplit("/", 1)[0]
assert st
vdev = self.dev
if not vdev:
vdev = self.dev = bos.stat(self.realpath).st_dev
if vdev != st.st_dev:
if self.log:
t = "xdev: {}[{}] => {}[{}]"
self.log("vfs", t.format(vdev, self.realpath, st.st_dev, ap), 3)
return None
if "xvol" in self.flags:
for vap, vn in self.root.all_aps:
if aps.startswith(vap):
return vn
if self.log:
self.log("vfs", "xvol: [{}]".format(ap), 3)
return None
return self
if WINDOWS:
re_vol = re.compile(r"^([a-zA-Z]:[\\/][^:]*|[^:]*):([^:]*):(.*)$")
@@ -770,6 +841,9 @@ class AuthSrv(object):
if not ln.split("#")[0].strip():
continue
if re.match(r"^\[.*\]:$", ln):
ln = ln[:-1]
subsection = ln in (catx, catf)
if ln.startswith("[") or subsection:
self._e()
@@ -1067,7 +1141,13 @@ class AuthSrv(object):
assert vfs
vfs.all_vols = {}
vfs.get_all_vols(vfs.all_vols)
vfs.all_aps = []
vfs.all_vps = []
vfs.get_all_vols(vfs.all_vols, vfs.all_aps, vfs.all_vps)
for vol in vfs.all_vols.values():
vol.all_aps.sort(key=lambda x: len(x[0]), reverse=True)
vol.all_vps.sort(key=lambda x: len(x[0]), reverse=True)
vol.root = vfs
for perm in "read write move del get pget".split():
axs_key = "u" + perm
@@ -1101,6 +1181,14 @@ class AuthSrv(object):
if LEELOO_DALLAS in all_users:
raise Exception("sorry, reserved username: " + LEELOO_DALLAS)
seenpwds = {}
for usr, pwd in acct.items():
if pwd in seenpwds:
t = "accounts [{}] and [{}] have the same password; this is not supported"
self.log(t.format(seenpwds[pwd], usr), 1)
raise Exception("invalid config")
seenpwds[pwd] = usr
promote = []
demote = []
for vol in vfs.all_vols.values():
@@ -1493,6 +1581,12 @@ class AuthSrv(object):
if t:
self.log("\n\033[{}\033[0m\n".format(t))
zv, _ = vfs.get("/", "*", False, False)
zs = zv.realpath.lower()
if zs in ("/", "c:\\") or zs.startswith(r"c:\windows"):
t = "you are sharing a system directory: {}\n"
self.log(t.format(zv.realpath), c=1)
try:
zv, _ = vfs.get("/", "*", False, True)
if self.warn_anonwrite and os.getcwd() == zv.realpath:

View File

@@ -13,6 +13,8 @@ def vf_bmap() -> dict[str, str]:
"no_dedup": "copydupes",
"no_dupe": "nodupe",
"no_forget": "noforget",
"dav_auth": "davauth",
"dav_rt": "davrt",
}
for k in (
"dotsrch",
@@ -106,7 +108,7 @@ flagcats = {
"dbd=[acid|swal|wal|yolo]": "database speed-durability tradeoff",
"xlink": "cross-volume dupe detection / linking",
"xdev": "do not descend into other filesystems",
"xvol": "skip symlinks leaving the volume root",
"xvol": "do not follow symlinks leaving the volume root",
"dotsrch": "show dotfiles in search results",
"nodotsrch": "hide dotfiles in search results (default)",
},
@@ -142,7 +144,9 @@ flagcats = {
"lg_sbf": "list of *logue-sandbox safeguards to disable",
},
"others": {
"fk=8": 'generates per-file accesskeys,\nwhich will then be required at the "g" permission'
"fk=8": 'generates per-file accesskeys,\nwhich will then be required at the "g" permission',
"davauth": "ask webdav clients to login for all folders",
"davrt": "show lastmod time of symlink destination, not the link itself\n(note: this option is always enabled for recursive listings)",
},
}

View File

@@ -2,6 +2,7 @@
from __future__ import print_function, unicode_literals
import argparse
import errno
import logging
import os
import stat
@@ -46,6 +47,12 @@ if True: # pylint: disable=using-constant-test
from typing import Any, Optional
class FSE(FilesystemError):
def __init__(self, msg: str, severity: int = 0) -> None:
super(FilesystemError, self).__init__(msg)
self.severity = severity
class FtpAuth(DummyAuthorizer):
def __init__(self, hub: "SvcHub") -> None:
super(FtpAuth, self).__init__()
@@ -87,7 +94,7 @@ class FtpAuth(DummyAuthorizer):
raise AuthenticationFailed("Authentication failed.")
handler.uname = uname
handler.uname = handler.username = uname
def get_home_dir(self, username: str) -> str:
return "/"
@@ -113,7 +120,8 @@ class FtpFs(AbstractedFS):
def __init__(
self, root: str, cmd_channel: Any
) -> None: # pylint: disable=super-init-not-called
self.h = self.cmd_channel = cmd_channel # type: FTPHandler
self.h = cmd_channel # type: FTPHandler
self.cmd_channel = cmd_channel # type: FTPHandler
self.hub: "SvcHub" = cmd_channel.hub
self.args = cmd_channel.args
self.uname = cmd_channel.uname
@@ -127,10 +135,6 @@ class FtpFs(AbstractedFS):
self.listdirinfo = self.listdir
self.chdir(".")
def die(self, msg):
self.h.die(msg)
raise Exception()
def v2a(
self,
vpath: str,
@@ -140,21 +144,34 @@ class FtpFs(AbstractedFS):
d: bool = False,
) -> tuple[str, VFS, str]:
try:
vpath = vpath.replace("\\", "/").lstrip("/")
vpath = vpath.replace("\\", "/").strip("/")
rd, fn = os.path.split(vpath)
if ANYWIN and relchk(rd):
logging.warning("malicious vpath: %s", vpath)
self.die("Unsupported characters in filepath")
t = "Unsupported characters in [{}]"
raise FSE(t.format(vpath), 1)
fn = sanitize_fn(fn or "", "", [".prologue.html", ".epilogue.html"])
vpath = vjoin(rd, fn)
vfs, rem = self.hub.asrv.vfs.get(vpath, self.uname, r, w, m, d)
if not vfs.realpath:
self.die("No filesystem mounted at this path")
t = "No filesystem mounted at [{}]"
raise FSE(t.format(vpath))
if "xdev" in vfs.flags or "xvol" in vfs.flags:
ap = vfs.canonical(rem)
avfs = vfs.chk_ap(ap)
t = "Permission denied in [{}]"
if not avfs:
raise FSE(t.format(vpath), 1)
cr, cw, cm, cd, _, _ = avfs.can_access("", self.h.uname)
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)
return os.path.join(vfs.realpath, rem), vfs, rem
except Pebkac as ex:
self.die(str(ex))
raise FSE(str(ex))
def rv2a(
self,
@@ -177,7 +194,7 @@ class FtpFs(AbstractedFS):
def validpath(self, path: str) -> bool:
if "/.hist/" in path:
if "/up2k." in path or path.endswith("/dir.txt"):
self.die("Access to this file is forbidden")
raise FSE("Access to this file is forbidden", 1)
return True
@@ -194,7 +211,7 @@ class FtpFs(AbstractedFS):
td = 0
if td < -1 or td > self.args.ftp_wt:
self.die("Cannot open existing file for writing")
raise FSE("Cannot open existing file for writing")
self.validpath(ap)
return open(fsenc(ap), mode)
@@ -203,9 +220,17 @@ class FtpFs(AbstractedFS):
nwd = join(self.cwd, path)
vfs, rem = self.hub.asrv.vfs.get(nwd, self.uname, False, False)
ap = vfs.canonical(rem)
if not bos.path.isdir(ap):
try:
st = bos.stat(ap)
if not stat.S_ISDIR(st.st_mode):
raise Exception()
except:
# returning 550 is library-default and suitable
self.die("Failed to change directory")
raise FSE("No such file or directory")
avfs = vfs.chk_ap(ap, st)
if not avfs:
raise FSE("Permission denied", 1)
self.cwd = nwd
(
@@ -215,16 +240,18 @@ class FtpFs(AbstractedFS):
self.can_delete,
self.can_get,
self.can_upget,
) = self.hub.asrv.vfs.can_access(self.cwd.lstrip("/"), self.h.uname)
) = avfs.can_access("", self.h.uname)
def mkdir(self, path: str) -> None:
ap = self.rv2a(path, w=True)[0]
bos.makedirs(ap) # filezilla expects this
def listdir(self, path: str) -> list[str]:
vpath = join(self.cwd, path).lstrip("/")
vpath = join(self.cwd, path)
try:
vfs, rem = self.hub.asrv.vfs.get(vpath, self.uname, True, False)
ap, vfs, rem = self.v2a(vpath, True, False)
if not bos.path.isdir(ap):
raise FSE("No such file or directory", 1)
fsroot, vfs_ls1, vfs_virt = vfs.ls(
rem,
@@ -240,8 +267,12 @@ class FtpFs(AbstractedFS):
vfs_ls.sort()
return vfs_ls
except:
if vpath:
except Exception as ex:
# panic on malicious names
if getattr(ex, "severity", 0):
raise
if vpath.strip("/"):
# display write-only folders as empty
return []
@@ -251,31 +282,35 @@ class FtpFs(AbstractedFS):
def rmdir(self, path: str) -> None:
ap = self.rv2a(path, d=True)[0]
bos.rmdir(ap)
try:
bos.rmdir(ap)
except OSError as e:
if e.errno != errno.ENOENT:
raise
def remove(self, path: str) -> None:
if self.args.no_del:
self.die("The delete feature is disabled in server config")
raise FSE("The delete feature is disabled in server config")
vp = join(self.cwd, path).lstrip("/")
try:
self.hub.up2k.handle_rm(self.uname, self.h.cli_ip, [vp], [])
self.hub.up2k.handle_rm(self.uname, self.h.cli_ip, [vp], [], False)
except Exception as ex:
self.die(str(ex))
raise FSE(str(ex))
def rename(self, src: str, dst: str) -> None:
if not self.can_move:
self.die("Not allowed for user " + self.h.uname)
raise FSE("Not allowed for user " + self.h.uname)
if self.args.no_mv:
self.die("The rename/move feature is disabled in server config")
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, svp, dvp)
except Exception as ex:
self.die(str(ex))
raise FSE(str(ex))
def chmod(self, path: str, mode: str) -> None:
pass
@@ -284,7 +319,10 @@ class FtpFs(AbstractedFS):
try:
ap = self.rv2a(path, r=True)[0]
return bos.stat(ap)
except:
except FSE as ex:
if ex.severity:
raise
ap = self.rv2a(path)[0]
st = bos.stat(ap)
if not stat.S_ISDIR(st.st_mode):
@@ -304,7 +342,10 @@ class FtpFs(AbstractedFS):
try:
st = self.stat(path)
return stat.S_ISREG(st.st_mode)
except:
except Exception as ex:
if getattr(ex, "severity", 0):
raise
return False # expected for mojibake in ftp_SIZE()
def islink(self, path: str) -> bool:
@@ -315,7 +356,10 @@ class FtpFs(AbstractedFS):
try:
st = self.stat(path)
return stat.S_ISDIR(st.st_mode)
except:
except Exception as ex:
if getattr(ex, "severity", 0):
raise
return True
def getsize(self, path: str) -> int:
@@ -365,14 +409,10 @@ class FtpHandler(FTPHandler):
# reduce non-debug logging
self.log_cmds_list = [x for x in self.log_cmds_list if x not in ("CWD", "XCWD")]
def die(self, msg):
self.respond("550 {}".format(msg))
raise FilesystemError(msg)
def ftp_STOR(self, file: str, mode: str = "w") -> Any:
# Optional[str]
vp = join(self.fs.cwd, file).lstrip("/")
ap, vfs, rem = self.fs.v2a(vp)
ap, vfs, rem = self.fs.v2a(vp, w=True)
self.vfs_map[ap] = vp
xbu = vfs.flags.get("xbu")
if xbu and not runhook(
@@ -388,7 +428,7 @@ class FtpHandler(FTPHandler):
0,
"",
):
self.die("Upload blocked by xbu server config")
raise FSE("Upload blocked by xbu server config")
# print("ftp_STOR: {} {} => {}".format(vp, mode, ap))
ret = FTPHandler.ftp_STOR(self, file, mode)
@@ -488,6 +528,9 @@ class Ftpd(object):
if "::" in ips:
ips.append("0.0.0.0")
if self.args.ftp4:
ips = [x for x in ips if ":" not in x]
ioloop = IOLoop()
for ip in ips:
for h, lp in hs:

View File

@@ -135,6 +135,7 @@ class HttpCli(object):
self.ouparam: dict[str, str] = {}
self.uparam: dict[str, str] = {}
self.cookies: dict[str, str] = {}
self.avn: Optional[VFS] = None
self.vpath = " "
self.uname = " "
self.pw = " "
@@ -219,7 +220,7 @@ class HttpCli(object):
try:
self.s.settimeout(2)
headerlines = read_header(self.sr)
headerlines = read_header(self.sr, self.args.s_thead, self.args.s_thead)
self.in_hdr_recv = False
if not headerlines:
return False
@@ -266,7 +267,7 @@ class HttpCli(object):
)
self.host = self.headers.get("host") or ""
if not self.host:
zs = "{}:{}".format(*list(self.s.getsockname()[:2]))
zs = "%s:%s" % self.s.getsockname()[:2]
self.host = zs[7:] if zs.startswith("::ffff:") else zs
n = self.args.rproxy
@@ -330,7 +331,7 @@ class HttpCli(object):
for k in arglist.split("&"):
if "=" in k:
k, zs = k.split("=", 1)
uparam[k.lower()] = zs.strip()
uparam[k.lower()] = unquotep(zs.strip().replace("+", " "))
else:
uparam[k.lower()] = ""
@@ -403,10 +404,21 @@ class HttpCli(object):
self.get_pwd_cookie(self.pw)
if self.is_rclone:
# dots: always include dotfiles if permitted
# lt: probably more important showing the correct timestamps of any dupes it just uploaded rather than the lastmod time of any non-copyparty-managed symlinks
# b: basic-browser if it tries to parse the html listing
uparam["dots"] = ""
uparam["lt"] = ""
uparam["b"] = ""
cookies["b"] = ""
vn, rem = self.asrv.vfs.get(self.vpath, self.uname, False, False)
if "xdev" in vn.flags or "xvol" in vn.flags:
ap = vn.canonical(rem)
avn = vn.chk_ap(ap)
else:
avn = vn
(
self.can_read,
self.can_write,
@@ -414,7 +426,12 @@ class HttpCli(object):
self.can_delete,
self.can_get,
self.can_upget,
) = self.asrv.vfs.can_access(self.vpath, self.uname)
) = (
avn.can_access("", self.uname) if avn else [False] * 6
)
self.avn = avn
self.s.settimeout(self.args.s_tbody or None)
try:
cors_k = self._cors()
@@ -530,7 +547,7 @@ class HttpCli(object):
mime: Optional[str] = None,
headers: Optional[dict[str, str]] = None,
) -> None:
response = ["{} {} {}".format(self.http_ver, status, HTTPCODE[status])]
response = ["%s %s %s" % (self.http_ver, status, HTTPCODE[status])]
if length is not None:
response.append("Content-Length: " + unicode(length))
@@ -554,11 +571,10 @@ class HttpCli(object):
self.out_headers["Content-Type"] = mime
for k, zs in list(self.out_headers.items()) + self.out_headerlist:
response.append("{}: {}".format(k, zs))
response.append("%s: %s" % (k, zs))
try:
# best practice to separate headers and body into different packets
self.s.settimeout(None)
self.s.sendall("\r\n".join(response).encode("utf-8") + b"\r\n\r\n")
except:
raise Pebkac(400, "client d/c while replying headers")
@@ -621,7 +637,7 @@ class HttpCli(object):
if not kv:
return ""
r = ["{}={}".format(k, quotep(zs)) if zs else k for k, zs in kv.items()]
r = ["%s=%s" % (k, quotep(zs)) if zs else k for k, zs in kv.items()]
return "?" + "&amp;".join(r)
def redirect(
@@ -710,7 +726,7 @@ class HttpCli(object):
def handle_get(self) -> bool:
if self.do_log:
logmsg = "{:4} {}".format(self.mode, self.req)
logmsg = "%-4s %s @%s" % (self.mode, self.req, self.uname)
if "range" in self.headers:
try:
@@ -809,17 +825,20 @@ class HttpCli(object):
def handle_propfind(self) -> bool:
if self.do_log:
self.log("PFIND " + self.req)
self.log("PFIND %s @%s" % (self.req, self.uname))
if self.args.no_dav:
raise Pebkac(405, "WebDAV is disabled in server config")
if not self.can_read and not self.can_write and not self.can_get:
if self.vpath:
self.log("inaccessible: [{}]".format(self.vpath))
raise Pebkac(401, "authenticate")
vn, rem = self.asrv.vfs.get(self.vpath, self.uname, False, False, err=401)
tap = vn.canonical(rem)
self.uparam["h"] = ""
if "davauth" in vn.flags and self.uname == "*":
self.can_read = self.can_write = self.can_get = False
if not self.can_read and not self.can_write and not self.can_get:
self.log("inaccessible: [{}]".format(self.vpath))
raise Pebkac(401, "authenticate")
from .dxml import parse_xml
@@ -863,17 +882,16 @@ class HttpCli(object):
]
props = set(props_lst)
vn, rem = self.asrv.vfs.get(self.vpath, self.uname, True, False, err=401)
depth = self.headers.get("depth", "infinity").lower()
try:
topdir = {"vp": "", "st": bos.stat(vn.canonical(rem))}
topdir = {"vp": "", "st": bos.stat(tap)}
except OSError as ex:
if ex.errno != errno.ENOENT:
if ex.errno not in (errno.ENOENT, errno.ENOTDIR):
raise
raise Pebkac(404)
if not stat.S_ISDIR(topdir["st"].st_mode):
if depth == "0" or not self.can_read or not stat.S_ISDIR(topdir["st"].st_mode):
fgen = []
elif depth == "infinity":
@@ -883,6 +901,9 @@ class HttpCli(object):
self.reply(zb, 403, "application/xml; charset=utf-8")
return True
# this will return symlink-target timestamps
# because lstat=true would not recurse into subfolders
# and this is a rare case where we actually want that
fgen = vn.zipgen(
rem,
rem,
@@ -896,7 +917,11 @@ class HttpCli(object):
elif depth == "1":
_, vfs_ls, vfs_virt = vn.ls(
rem, self.uname, not self.args.no_scandir, [[True, False]]
rem,
self.uname,
not self.args.no_scandir,
[[True, False]],
lstat="davrt" not in vn.flags,
)
if not self.args.ed:
names = set(exclude_dotfiles([x[0] for x in vfs_ls]))
@@ -908,9 +933,6 @@ class HttpCli(object):
ls += [{"vp": v, "st": zsr} for v in vfs_virt]
fgen = ls # type: ignore
elif depth == "0":
fgen = [] # type: ignore
else:
t = "invalid depth value '{}' (must be either '0' or '1'{})"
t2 = " or 'infinity'" if self.args.dav_inf else ""
@@ -930,14 +952,23 @@ class HttpCli(object):
for x in fgen:
rp = vjoin(vtop, x["vp"])
st: os.stat_result = x["st"]
mtime = st.st_mtime
if stat.S_ISLNK(st.st_mode):
try:
st = bos.stat(os.path.join(tap, x["vp"]))
except:
continue
isdir = stat.S_ISDIR(st.st_mode)
t = "<D:response><D:href>/{}{}</D:href><D:propstat><D:prop>"
ret += t.format(quotep(rp), "/" if isdir and rp else "")
ret += "<D:response><D:href>/%s%s</D:href><D:propstat><D:prop>" % (
quotep(rp),
"/" if isdir and rp else "",
)
pvs: dict[str, str] = {
"displayname": html_escape(rp.split("/")[-1]),
"getlastmodified": formatdate(st.st_mtime, usegmt=True),
"getlastmodified": formatdate(mtime, usegmt=True),
"resourcetype": '<D:collection xmlns:D="DAV:"/>' if isdir else "",
"supportedlock": '<D:lockentry xmlns:D="DAV:"><D:lockscope><D:exclusive/></D:lockscope><D:locktype><D:write/></D:locktype></D:lockentry>',
}
@@ -949,13 +980,13 @@ class HttpCli(object):
if k not in props:
continue
elif v:
ret += "<D:{0}>{1}</D:{0}>".format(k, v)
ret += "<D:%s>%s</D:%s>" % (k, v, k)
else:
ret += "<D:{}/>".format(k)
ret += "<D:%s/>" % (k,)
ret += "</D:prop><D:status>HTTP/1.1 200 OK</D:status></D:propstat>"
missing = ["<D:{}/>".format(x) for x in props if x not in pvs]
missing = ["<D:%s/>" % (x,) for x in props if x not in pvs]
if missing and clen:
t = "<D:propstat><D:prop>{}</D:prop><D:status>HTTP/1.1 404 Not Found</D:status></D:propstat>"
ret += t.format("".join(missing))
@@ -974,7 +1005,7 @@ class HttpCli(object):
def handle_proppatch(self) -> bool:
if self.do_log:
self.log("PPATCH " + self.req)
self.log("PPATCH %s @%s" % (self.req, self.uname))
if self.args.no_dav:
raise Pebkac(405, "WebDAV is disabled in server config")
@@ -1032,7 +1063,7 @@ class HttpCli(object):
def handle_lock(self) -> bool:
if self.do_log:
self.log("LOCK " + self.req)
self.log("LOCK %s @%s" % (self.req, self.uname))
if self.args.no_dav:
raise Pebkac(405, "WebDAV is disabled in server config")
@@ -1098,7 +1129,7 @@ class HttpCli(object):
def handle_unlock(self) -> bool:
if self.do_log:
self.log("UNLOCK " + self.req)
self.log("UNLOCK %s @%s" % (self.req, self.uname))
if self.args.no_dav:
raise Pebkac(405, "WebDAV is disabled in server config")
@@ -1115,7 +1146,7 @@ class HttpCli(object):
return True
if self.do_log:
self.log("MKCOL " + self.req)
self.log("MKCOL %s @%s" % (self.req, self.uname))
try:
return self._mkdir(self.vpath, True)
@@ -1130,18 +1161,9 @@ class HttpCli(object):
dst = self.headers["destination"]
dst = re.sub("^https?://[^/]+", "", dst).lstrip()
dst = unquotep(dst)
if not self._mv(self.vpath, dst):
if not self._mv(self.vpath, dst.lstrip("/")):
return False
# up2k only cares about files and removes all empty folders;
# clients naturally expect empty folders to survive a rename
vn, rem = self.asrv.vfs.get(dst, self.uname, False, False)
dabs = vn.canonical(rem)
try:
bos.makedirs(dabs)
except:
pass
return True
def _applesan(self) -> bool:
@@ -1176,7 +1198,7 @@ class HttpCli(object):
def handle_options(self) -> bool:
if self.do_log:
self.log("OPTIONS " + self.req)
self.log("OPTIONS %s @%s" % (self.req, self.uname))
oh = self.out_headers
oh["Allow"] = ", ".join(self.conn.hsrv.mallow)
@@ -1191,11 +1213,11 @@ class HttpCli(object):
return True
def handle_delete(self) -> bool:
self.log("DELETE " + self.req)
self.log("DELETE %s @%s" % (self.req, self.uname))
return self.handle_rm([])
def handle_put(self) -> bool:
self.log("PUT " + self.req)
self.log("PUT %s @%s" % (self.req, self.uname))
if not self.can_write:
t = "user {} does not have write-access here"
@@ -1206,7 +1228,6 @@ class HttpCli(object):
if self.headers.get("expect", "").lower() == "100-continue":
try:
self.s.settimeout(None)
self.s.sendall(b"HTTP/1.1 100 Continue\r\n\r\n")
except:
raise Pebkac(400, "client d/c before 100 continue")
@@ -1214,11 +1235,10 @@ class HttpCli(object):
return self.handle_stash(True)
def handle_post(self) -> bool:
self.log("POST " + self.req)
self.log("POST %s @%s" % (self.req, self.uname))
if self.headers.get("expect", "").lower() == "100-continue":
try:
self.s.settimeout(None)
self.s.sendall(b"HTTP/1.1 100 Continue\r\n\r\n")
except:
raise Pebkac(400, "client d/c before 100 continue")
@@ -1623,7 +1643,7 @@ class HttpCli(object):
spd1 = get_spd(nbytes, self.t0)
spd2 = get_spd(self.conn.nbyte, self.conn.t0)
return "{} {} n{}".format(spd1, spd2, self.conn.nreq)
return "%s %s n%s" % (spd1, spd2, self.conn.nreq)
def handle_post_multipart(self) -> bool:
self.parser = MultipartParser(self.log, self.sr, self.headers)
@@ -2758,7 +2778,7 @@ class HttpCli(object):
return True
def tx_md(self, fs_path: str) -> bool:
logmsg = "{:4} {} ".format("", self.req)
logmsg = " %s @%s " % (self.req, self.uname)
if not self.can_write:
if "edit" in self.uparam or "edit2" in self.uparam:
@@ -3034,8 +3054,8 @@ class HttpCli(object):
ret = self.gen_tree(top, dst)
if self.is_vproxied:
parents = self.args.R.split("/")
for parent in parents[::-1]:
ret = {"k{}".format(parent): ret, "a": []}
for parent in reversed(parents):
ret = {"k%s" % (parent,): ret, "a": []}
zs = json.dumps(ret)
self.reply(zs.encode("utf-8"), mime="application/json")
@@ -3173,7 +3193,9 @@ class HttpCli(object):
nlim = int(self.uparam.get("lim") or 0)
lim = [nlim, nlim] if nlim else []
x = self.conn.hsrv.broker.ask("up2k.handle_rm", self.uname, self.ip, req, lim)
x = self.conn.hsrv.broker.ask(
"up2k.handle_rm", self.uname, self.ip, req, lim, False
)
self.loud_reply(x.get())
return True
@@ -3190,7 +3212,7 @@ class HttpCli(object):
# x-www-form-urlencoded (url query part) uses
# either + or %20 for 0x20 so handle both
dst = unquotep(dst.replace("+", " "))
return self._mv(self.vpath, dst)
return self._mv(self.vpath, dst.lstrip("/"))
def _mv(self, vsrc: str, vdst: str) -> bool:
if not self.can_move:
@@ -3514,7 +3536,11 @@ class HttpCli(object):
return self.tx_zip(k, v, self.vpath, vn, rem, [], self.args.ed)
fsroot, vfs_ls, vfs_virt = vn.ls(
rem, self.uname, not self.args.no_scandir, [[True, False], [False, True]]
rem,
self.uname,
not self.args.no_scandir,
[[True, False], [False, True]],
lstat="lt" in self.uparam,
)
stats = {k: v for k, v in vfs_ls}
ls_names = [x[0] for x in vfs_ls]
@@ -3558,7 +3584,8 @@ class HttpCli(object):
fspath = fsroot + "/" + fn
try:
inf = stats.get(fn) or bos.stat(fspath)
linf = stats.get(fn) or bos.lstat(fspath)
inf = bos.stat(fspath) if stat.S_ISLNK(linf.st_mode) else linf
except:
self.log("broken symlink: {}".format(repr(fspath)))
continue
@@ -3569,19 +3596,26 @@ class HttpCli(object):
if self.args.no_zip:
margin = "DIR"
else:
margin = '<a href="{}?zip" rel="nofollow">zip</a>'.format(
quotep(href)
)
margin = '<a href="%s?zip" rel="nofollow">zip</a>' % (quotep(href),)
elif fn in hist:
margin = '<a href="{}.hist/{}">#{}</a>'.format(
base, html_escape(hist[fn][2], quot=True, crlf=True), hist[fn][0]
margin = '<a href="%s.hist/%s">#%s</a>' % (
base,
html_escape(hist[fn][2], quot=True, crlf=True),
hist[fn][0],
)
else:
margin = "-"
sz = inf.st_size
zd = datetime.utcfromtimestamp(inf.st_mtime)
dt = zd.strftime("%Y-%m-%d %H:%M:%S")
zd = datetime.utcfromtimestamp(linf.st_mtime)
dt = "%04d-%02d-%02d %02d:%02d:%02d" % (
zd.year,
zd.month,
zd.day,
zd.hour,
zd.minute,
zd.second,
)
try:
ext = "---" if is_dir else fn.rsplit(".", 1)[1]
@@ -3591,7 +3625,7 @@ class HttpCli(object):
ext = "%"
if add_fk:
href = "{}?k={}".format(
href = "%s?k=%s" % (
quotep(href),
self.gen_fk(
self.args.fk_salt, fspath, sz, 0 if ANYWIN else inf.st_ino
@@ -3607,7 +3641,7 @@ class HttpCli(object):
"sz": sz,
"ext": ext,
"dt": dt,
"ts": int(inf.st_mtime),
"ts": int(linf.st_mtime),
}
if is_dir:
dirs.append(item)

View File

View File

@@ -261,7 +261,7 @@ class SMB(object):
yeet("blocked delete (no-del-acc): " + vpath)
vpath = vpath.replace("\\", "/").lstrip("/")
self.hub.up2k.handle_rm(LEELOO_DALLAS, "1.7.6.2", [vpath], [])
self.hub.up2k.handle_rm(LEELOO_DALLAS, "1.7.6.2", [vpath], [], False)
def _utime(self, vpath: str, times: tuple[float, float]) -> None:
if not self.args.smbw:

View File

@@ -647,8 +647,14 @@ class SvcHub(object):
return
with self.log_mutex:
ts = datetime.utcnow().strftime("%Y-%m%d-%H%M%S.%f")[:-3]
self.logf.write("@{} [{}\033[0m] {}\n".format(ts, src, msg))
zd = datetime.utcnow()
ts = "%04d-%04d-%06d.%03d" % (
zd.year,
zd.month * 100 + zd.day,
(zd.hour * 100 + zd.minute) * 100 + zd.second,
zd.microsecond // 1000,
)
self.logf.write("@%s [%s\033[0m] %s\n" % (ts, src, msg))
now = time.time()
if now >= self.next_day:
@@ -678,23 +684,29 @@ class SvcHub(object):
print("\033[36m{}\033[0m\n".format(dt.strftime("%Y-%m-%d")), end="")
self._set_next_day()
fmt = "\033[36m{} \033[33m{:21} \033[0m{}\n"
fmt = "\033[36m%s \033[33m%-21s \033[0m%s\n"
if not VT100:
fmt = "{} {:21} {}\n"
fmt = "%s %-21s %s\n"
if "\033" in msg:
msg = ansi_re.sub("", msg)
if "\033" in src:
src = ansi_re.sub("", src)
elif c:
if isinstance(c, int):
msg = "\033[3{}m{}\033[0m".format(c, msg)
msg = "\033[3%sm%s\033[0m" % (c, msg)
elif "\033" not in c:
msg = "\033[{}m{}\033[0m".format(c, msg)
msg = "\033[%sm%s\033[0m" % (c, msg)
else:
msg = "{}{}\033[0m".format(c, msg)
msg = "%s%s\033[0m" % (c, msg)
ts = datetime.utcfromtimestamp(now).strftime("%H:%M:%S.%f")[:-3]
msg = fmt.format(ts, src, msg)
zd = datetime.utcfromtimestamp(now)
ts = "%02d:%02d:%02d.%03d" % (
zd.hour,
zd.minute,
zd.second,
zd.microsecond // 1000,
)
msg = fmt % (ts, src, msg)
try:
print(msg, end="")
except UnicodeEncodeError:

View File

@@ -274,6 +274,10 @@ class ThumbSrv(object):
tdir, tfn = os.path.split(tpath)
ttpath = os.path.join(tdir, "w", tfn)
try:
bos.unlink(ttpath)
except:
pass
for fun in funs:
try:
@@ -570,11 +574,15 @@ class ThumbSrv(object):
want_caf = tpath.endswith(".caf")
tmp_opus = tpath
if want_caf:
tmp_opus = tpath.rsplit(".", 1)[0] + ".opus"
tmp_opus = tpath + ".opus"
try:
bos.unlink(tmp_opus)
except:
pass
caf_src = abspath if src_opus else tmp_opus
if not want_caf or (not src_opus and not bos.path.isfile(tmp_opus)):
if not want_caf or not src_opus:
# fmt: off
cmd = [
b"ffmpeg",
@@ -633,6 +641,12 @@ class ThumbSrv(object):
# fmt: on
self._run_ff(cmd)
if tmp_opus != tpath:
try:
bos.unlink(tmp_opus)
except:
pass
def poke(self, tdir: str) -> None:
if not self.poke_cd.poke(tdir):
return

View File

@@ -73,8 +73,8 @@ if True: # pylint: disable=using-constant-test
if TYPE_CHECKING:
from .svchub import SvcHub
zs = "avif,avifs,bmp,gif,heic,heics,heif,heifs,ico,j2p,j2k,jp2,jpeg,jpg,jpx,png,tga,tif,tiff,webp"
CV_EXTS = set(zs.split(","))
zsg = "avif,avifs,bmp,gif,heic,heics,heif,heifs,ico,j2p,j2k,jp2,jpeg,jpg,jpx,png,tga,tif,tiff,webp"
CV_EXTS = set(zsg.split(","))
class Dbw(object):
@@ -380,11 +380,11 @@ class Up2k(object):
if rd.startswith("//") or fn.startswith("//"):
rd, fn = s3dec(rd, fn)
fvp = "{}/{}".format(rd, fn).strip("/")
fvp = ("%s/%s" % (rd, fn)).strip("/")
if vp:
fvp = "{}/{}".format(vp, fvp)
fvp = "%s/%s" % (vp, fvp)
self._handle_rm(LEELOO_DALLAS, "", fvp, [])
self._handle_rm(LEELOO_DALLAS, "", fvp, [], True)
nrm += 1
if nrm:
@@ -2564,7 +2564,7 @@ class Up2k(object):
try:
if "hardlink" in flags:
os.link(fsenc(src), fsenc(dst))
os.link(fsenc(absreal(src)), fsenc(dst))
linked = True
except Exception as ex:
self.log("cannot hardlink: " + repr(ex))
@@ -2897,7 +2897,9 @@ class Up2k(object):
except:
pass
def handle_rm(self, uname: str, ip: str, vpaths: list[str], lim: list[int]) -> str:
def handle_rm(
self, uname: str, ip: str, vpaths: list[str], lim: list[int], rm_up: bool
) -> str:
n_files = 0
ok = {}
ng = {}
@@ -2906,7 +2908,7 @@ class Up2k(object):
self.log("hit delete limit of {} files".format(lim[1]), 3)
break
a, b, c = self._handle_rm(uname, ip, vp, lim)
a, b, c = self._handle_rm(uname, ip, vp, lim, rm_up)
n_files += a
for k in b:
ok[k] = 1
@@ -2920,7 +2922,7 @@ class Up2k(object):
return "deleted {} files (and {}/{} folders)".format(n_files, iok, iok + ing)
def _handle_rm(
self, uname: str, ip: str, vpath: str, lim: list[int]
self, uname: str, ip: str, vpath: str, lim: list[int], rm_up: bool
) -> tuple[int, list[str], list[str]]:
self.db_act = time.time()
try:
@@ -3027,16 +3029,22 @@ class Up2k(object):
if xad:
runhook(self.log, xad, abspath, vpath, "", uname, 0, 0, ip, 0, "")
ok: list[str] = []
ng: list[str] = []
if is_dir:
ok, ng = rmdirs(self.log_func, scandir, True, atop, 1)
else:
ok = ng = []
ok2, ng2 = rmdirs_up(os.path.dirname(atop), ptop)
if rm_up:
ok2, ng2 = rmdirs_up(os.path.dirname(atop), ptop)
else:
ok2 = ng2 = []
return n_files, ok + ok2, ng + ng2
def handle_mv(self, uname: str, svp: str, dvp: str) -> str:
if svp == dvp or dvp.startswith(svp + "/"):
raise Pebkac(400, "mv: cannot move parent into subfolder")
svn, srem = self.asrv.vfs.get(svp, uname, True, False, True)
svn, srem = svn.get_dbv(srem)
sabs = svn.canonical(srem, False)
@@ -3090,8 +3098,21 @@ class Up2k(object):
curs.clear()
rmdirs(self.log_func, scandir, True, sabs, 1)
rmdirs_up(os.path.dirname(sabs), svn.realpath)
rm_ok, rm_ng = rmdirs(self.log_func, scandir, True, sabs, 1)
for zsl in (rm_ok, rm_ng):
for ap in reversed(zsl):
if not ap.startswith(sabs):
raise Pebkac(500, "mv_d: bug at {}, top {}".format(ap, sabs))
rem = ap[len(sabs) :].replace(os.sep, "/").lstrip("/")
vp = vjoin(dvp, rem)
try:
dvn, drem = self.asrv.vfs.get(vp, uname, False, True)
bos.mkdir(dvn.canonical(drem))
except:
pass
return "k"
def _mv_file(

View File

@@ -296,11 +296,11 @@ REKOBO_LKEY = {k.lower(): v for k, v in REKOBO_KEY.items()}
pybin = sys.executable or ""
if EXE:
pybin = ""
for p in "python3 python".split():
for zsg in "python3 python".split():
try:
p = shutil.which(p)
if p:
pybin = p
zsg = shutil.which(zsg)
if zsg:
pybin = zsg
break
except:
pass
@@ -537,7 +537,7 @@ class _Unrecv(object):
self.log = log
self.buf: bytes = b""
def recv(self, nbytes: int) -> bytes:
def recv(self, nbytes: int, spins: int = 1) -> bytes:
if self.buf:
ret = self.buf[:nbytes]
self.buf = self.buf[nbytes:]
@@ -548,6 +548,10 @@ class _Unrecv(object):
ret = self.s.recv(nbytes)
break
except socket.timeout:
spins -= 1
if spins <= 0:
ret = b""
break
continue
except:
ret = b""
@@ -590,7 +594,7 @@ class _LUnrecv(object):
self.log = log
self.buf = b""
def recv(self, nbytes: int) -> bytes:
def recv(self, nbytes: int, spins: int) -> bytes:
if self.buf:
ret = self.buf[:nbytes]
self.buf = self.buf[nbytes:]
@@ -609,7 +613,7 @@ class _LUnrecv(object):
def recv_ex(self, nbytes: int, raise_on_trunc: bool = True) -> bytes:
"""read an exact number of bytes"""
try:
ret = self.recv(nbytes)
ret = self.recv(nbytes, 1)
err = False
except:
ret = b""
@@ -617,7 +621,7 @@ class _LUnrecv(object):
while not err and len(ret) < nbytes:
try:
ret += self.recv(nbytes - len(ret))
ret += self.recv(nbytes - len(ret), 1)
except OSError:
err = True
@@ -1292,7 +1296,7 @@ class MultipartParser(object):
rfc1341/rfc1521/rfc2047/rfc2231/rfc2388/rfc6266/the-real-world
(only the fallback non-js uploader relies on these filenames)
"""
for ln in read_header(self.sr):
for ln in read_header(self.sr, 2, 2592000):
self.log(ln)
m = self.re_ctype.match(ln)
@@ -1492,15 +1496,15 @@ def get_boundary(headers: dict[str, str]) -> str:
return m.group(2)
def read_header(sr: Unrecv) -> list[str]:
def read_header(sr: Unrecv, t_idle: int, t_tot: int) -> list[str]:
t0 = time.time()
ret = b""
while True:
if time.time() - t0 > 120:
if time.time() - t0 >= t_tot:
return []
try:
ret += sr.recv(1024)
ret += sr.recv(1024, t_idle // 2)
except:
if not ret:
return []
@@ -1549,7 +1553,7 @@ def rand_name(fdir: str, fn: str, rnd: int) -> str:
def gen_filekey(salt: str, fspath: str, fsize: int, inode: int) -> str:
return base64.urlsafe_b64encode(
hashlib.sha512(
"{} {} {} {}".format(salt, fspath, fsize, inode).encode("utf-8", "replace")
("%s %s %s %s" % (salt, fspath, fsize, inode)).encode("utf-8", "replace")
).digest()
).decode("ascii")
@@ -1589,7 +1593,7 @@ def gen_filekey_dbg(
def gencookie(k: str, v: str, r: str, tls: bool, dur: Optional[int]) -> str:
v = v.replace(";", "")
v = v.replace("%", "%25").replace(";", "%3B")
if dur:
exp = formatdate(time.time() + dur, usegmt=True)
else:
@@ -1658,7 +1662,7 @@ def uncyg(path: str) -> str:
if len(path) > 2 and path[2] != "/":
return path
return "{}:\\{}".format(path[1], path[3:])
return "%s:\\%s" % (path[1], path[3:])
def undot(path: str) -> str:
@@ -1701,7 +1705,7 @@ def sanitize_fn(fn: str, ok: str, bad: list[str]) -> str:
bad = ["con", "prn", "aux", "nul"]
for n in range(1, 10):
bad += "com{0} lpt{0}".format(n).split(" ")
bad += ("com%s lpt%s" % (n, n)).split(" ")
if fn.lower().split(".")[0] in bad:
fn = "_" + fn
@@ -2266,7 +2270,7 @@ def rmdirs(
dirs = [os.path.join(top, x) for x in dirs]
ok = []
ng = []
for d in dirs[::-1]:
for d in reversed(dirs):
a, b = rmdirs(logger, scandir, lstat, d, depth + 1)
ok += a
ng += b
@@ -2316,7 +2320,7 @@ def unescape_cookie(orig: str) -> str:
ret += chr(int(esc[1:], 16))
except:
ret += esc
esc = ""
esc = ""
else:
ret += ch

View File

@@ -6,7 +6,7 @@ pk: $(addsuffix .gz, $(wildcard *.js *.css))
un: $(addsuffix .un, $(wildcard *.gz))
%.gz: %
pigz -11 -J 34 -I 5730 $<
pigz -11 -J 34 -I 573 $<
%.un: %
pigz -d $<

1
copyparty/web/a/u2c.py Symbolic link
View File

@@ -0,0 +1 @@
../../../bin/u2c.py

View File

@@ -1 +0,0 @@
../../../bin/up2k.py

View File

@@ -1159,10 +1159,10 @@ html.y #widget.open {
background: #fff;
background: var(--bg-u3);
}
#wfm, #wzip, #wnp {
#wfs, #wfm, #wzip, #wnp {
display: none;
}
#wzip, #wnp {
#wfs, #wzip, #wnp {
margin-right: .2em;
padding-right: .2em;
border: 1px solid var(--bg-u5);
@@ -1174,6 +1174,7 @@ html.y #widget.open {
padding-left: .2em;
border-left-width: .1em;
}
#wfs.act,
#wfm.act {
display: inline-block;
}
@@ -1197,6 +1198,13 @@ html.y #widget.open {
position: relative;
display: inline-block;
}
#wfs {
font-size: .36em;
text-align: right;
line-height: 1.3em;
padding: 0 .3em 0 0;
border-width: 0 .25em 0 0;
}
#wfm span,
#wnp span {
font-size: .6em;

View File

@@ -261,6 +261,7 @@ var Ls = {
"mm_e403": "Could not play audio; error 403: Access denied.\n\nTry pressing F5 to reload, maybe you got logged out",
"mm_e5xx": "Could not play audio; server error ",
"mm_nof": "not finding any more audio files nearby",
"mm_pwrsv": "<p>it looks like playback is being interrupted by your phone's power-saving settings!</p>" + '<p>please go to <a target="_blank" href="https://user-images.githubusercontent.com/241032/235262121-2ffc51ae-7821-4310-a322-c3b7a507890c.png">the app settings of your browser</a> and then <a target="_blank" href="https://user-images.githubusercontent.com/241032/235262123-c328cca9-3930-4948-bd18-3949b9fd3fcf.png">allow unrestricted battery usage</a> to fix it.</p><p>(probably a good idea to use a separate browser dedicated for just music streaming...)</p>',
"mm_hnf": "that song no longer exists",
"im_hnf": "that image no longer exists",
@@ -721,6 +722,7 @@ var Ls = {
"mm_e403": "Avspilling feilet: Tilgang nektet.\n\nKanskje du ble logget ut?\nPrøv å trykk F5 for å laste siden på nytt.",
"mm_e5xx": "Avspilling feilet: ",
"mm_nof": "finner ikke flere sanger i nærheten",
"mm_pwrsv": "<p>det ser ut som musikken ble avbrutt av telefonen sine strømsparings-innstillinger!</p>" + '<p>ta en tur innom <a target="_blank" href="https://user-images.githubusercontent.com/241032/235262121-2ffc51ae-7821-4310-a322-c3b7a507890c.png">app-innstillingene til nettleseren din</a> og så <a target="_blank" href="https://user-images.githubusercontent.com/241032/235262123-c328cca9-3930-4948-bd18-3949b9fd3fcf.png">tillat ubegrenset batteriforbruk</a></p><p>(sikkert smart å ha en egen nettleser kun for musikkspilling...)</p>',
"mm_hnf": "sangen finnes ikke lenger",
"im_hnf": "bildet finnes ikke lenger",
@@ -952,6 +954,7 @@ ebi('ops').innerHTML = (
// media player
ebi('widget').innerHTML = (
'<div id="wtoggle">' +
'<span id="wfs"></span>' +
'<span id="wfm"><a' +
' href="#" id="fren" tt="' + L.wt_ren + '">✎<span>name</span></a><a' +
' href="#" id="fdel" tt="' + L.wt_del + '">⌫<span>del.</span></a><a' +
@@ -1482,7 +1485,8 @@ var mpl = (function () {
ebi('np_title').textContent = np.title || '';
ebi('np_dur').textContent = np['.dur'] || '';
ebi('np_url').textContent = get_vpath() + np.file.split('?')[0];
ebi('np_img').setAttribute('src', cover); // dont give last.fm the pwd
if (!MOBILE)
ebi('np_img').setAttribute('src', cover || ''); // dont give last.fm the pwd
navigator.mediaSession.metadata = new MediaMetadata(tags);
navigator.mediaSession.setActionHandler('play', mplay);
@@ -1499,6 +1503,7 @@ var mpl = (function () {
if (!r.os_ctl)
return;
// dead code; left for debug
navigator.mediaSession.metadata = null;
navigator.mediaSession.playbackState = "paused";
@@ -1538,12 +1543,14 @@ var re_au_native = can_ogg ? /\.(aac|flac|m4a|mp3|ogg|opus|wav)$/i :
// extract songs + add play column
var mpo = { "au": null, "au2": null, "acs": null };
var t_fchg = 0;
function MPlayer() {
var r = this;
r.id = Date.now();
r.au = null;
r.au = null;
r.au2 = null;
r.au = mpo.au;
r.au2 = mpo.au2;
r.acs = mpo.acs;
r.tracks = {};
r.order = [];
r.cd_pause = 0;
@@ -1833,6 +1840,7 @@ var pbar = (function () {
html_txt = 'a',
lastmove = 0,
mousepos = 0,
t_redraw = 0,
gradh = -1,
grad;
@@ -1901,6 +1909,9 @@ var pbar = (function () {
bctx = bc.ctx,
apos, adur;
if (!widget.is_open)
return;
bctx.clearRect(0, 0, bc.w, bc.h);
if (!mp || !mp.au || !isNum(adur = mp.au.duration) || !isNum(apos = mp.au.currentTime) || apos < 0 || adur < apos)
@@ -1963,6 +1974,10 @@ var pbar = (function () {
w = 8,
apos, adur;
if (t_redraw) {
clearTimeout(t_redraw);
t_redraw = 0;
}
pctx.clearRect(0, 0, pc.w, pc.h);
if (!mp || !mp.au || !isNum(adur = mp.au.duration) || !isNum(apos = mp.au.currentTime) || apos < 0 || adur < apos)
@@ -1977,17 +1992,30 @@ var pbar = (function () {
}
var sm = bc.w * 1.0 / adur,
t1 = s2ms(adur),
t2 = s2ms(apos),
x = sm * apos;
if (w && html_txt != t2) {
ebi('np_pos').textContent = html_txt = t2;
if (mpl.os_ctl)
navigator.mediaSession.setPositionState({
'duration': adur,
'position': apos,
'playbackRate': 1
});
}
if (!widget.is_open)
return;
pctx.fillStyle = '#573'; pctx.fillRect((x - w / 2) - 1, 0, w + 2, pc.h);
pctx.fillStyle = '#dfc'; pctx.fillRect((x - w / 2), 0, w, pc.h);
pctx.lineWidth = 2.5;
pctx.fillStyle = '#fff';
var t1 = s2ms(adur),
t2 = s2ms(apos),
m1 = pctx.measureText(t1),
var m1 = pctx.measureText(t1),
m1b = pctx.measureText(t1 + ":88"),
m2 = pctx.measureText(t2),
yt = pc.h / 3 * 2.1,
@@ -2001,15 +2029,8 @@ var pbar = (function () {
pctx.fillText(t1, xt1, yt);
pctx.fillText(t2, xt2, yt);
if (w && html_txt != t2) {
ebi('np_pos').textContent = html_txt = t2;
if (mpl.os_ctl)
navigator.mediaSession.setPositionState({
'duration': adur,
'position': apos,
'playbackRate': 1
});
}
if (sm > 10)
t_redraw = setTimeout(r.drawpos, sm > 50 ? 20 : 50);
};
window.addEventListener('resize', r.onresize);
@@ -2159,17 +2180,31 @@ function song_skip(n) {
else
play(mp.order[n == -1 ? mp.order.length - 1 : 0]);
}
function next_song_sig(e) {
t_fchg = document.hasFocus() ? 0 : Date.now();
return next_song_cmn(e);
}
function next_song(e) {
t_fchg = 0;
return next_song_cmn(e);
}
function next_song_cmn(e) {
ev(e);
if (mp.order.length) {
mpl.traversals = 0;
return song_skip(1);
}
if (mpl.traversals++ < 5) {
treectl.ls_cb = next_song;
if (MOBILE && t_fchg && Date.now() - t_fchg > 30 * 1000)
modal.alert(L.mm_pwrsv);
t_fchg = document.hasFocus() ? 0 : Date.now();
treectl.ls_cb = next_song_cmn;
return tree_neigh(1);
}
toast.inf(10, L.mm_nof);
mpl.traversals = 0;
t_fchg = 0;
}
function prev_song(e) {
ev(e);
@@ -2285,10 +2320,16 @@ var mpui = (function () {
return;
}
var paint = !MOBILE || document.hasFocus();
var pos = mp.au.currentTime;
if (!isNum(pos))
pos = 0;
// indicate playback state in ui
widget.paused(mp.au.paused);
if (++nth > 69) {
if (paint && ++nth > 69) {
// android-chrome breaks aspect ratio with unannounced viewport changes
nth = 0;
if (MOBILE) {
@@ -2297,20 +2338,28 @@ var mpui = (function () {
vbar.onresize();
}
}
else {
else if (paint) {
// draw current position in song
if (!mp.au.paused)
pbar.drawpos();
// occasionally draw buffered regions
if (++nth % 5 == 0)
if (nth % 5 == 0)
pbar.drawbuf();
}
if (pos > 0.3 && t_fchg) {
// cannot check document.hasFocus to avoid false positives;
// it continues on power-on, doesn't need to be in-browser
if (MOBILE && Date.now() - t_fchg > 30 * 1000)
modal.alert(L.mm_pwrsv);
t_fchg = 0;
}
// preload next song
if (mpl.preload && preloaded != mp.au.rsrc) {
var pos = mp.au.currentTime,
len = mp.au.duration,
var len = mp.au.duration,
rem = pos > 1 ? len - pos : 999,
full = null;
@@ -2483,7 +2532,7 @@ var afilt = (function () {
if (mp.acs)
mp.acs.disconnect();
mp.acs = null;
mp.acs = mpo.acs = null;
};
r.apply = function () {
@@ -2703,6 +2752,7 @@ function play(tid, is_ev, seek) {
tn = 0;
}
else if (mpl.pb_mode == 'next') {
t_fchg = document.hasFocus() ? 0 : Date.now();
treectl.ls_cb = next_song;
return tree_neigh(1);
}
@@ -2722,7 +2772,9 @@ function play(tid, is_ev, seek) {
if (mp.au) {
mp.au.pause();
clmod(ebi('a' + mp.au.tid), 'act');
var el = ebi('a' + mp.au.tid);
if (el)
clmod(el, 'act');
}
else {
mp.au = new Audio();
@@ -2730,7 +2782,7 @@ function play(tid, is_ev, seek) {
mp.au.onerror = evau_error;
mp.au.onprogress = pbar.drawpos;
mp.au.onplaying = mpui.progress_updater;
mp.au.onended = next_song;
mp.au.onended = next_song_sig;
widget.open();
}
@@ -2747,7 +2799,7 @@ function play(tid, is_ev, seek) {
mp.au.onerror = evau_error;
mp.au.onprogress = pbar.drawpos;
mp.au.onplaying = mpui.progress_updater;
mp.au.onended = next_song;
mp.au.onended = next_song_sig;
t = mp.au.currentTime;
if (isNum(t) && t > 0.1)
mp.au.currentTime = 0;
@@ -2807,7 +2859,7 @@ function play(tid, is_ev, seek) {
toast.err(0, esc(L.mm_playerr + basenames(ex)));
}
clmod(ebi(oid), 'act');
setTimeout(next_song, 5000);
setTimeout(next_song_sig, 5000);
}
@@ -2895,7 +2947,7 @@ function autoplay_blocked(seek) {
modal.confirm('<h6>' + L.mm_hashplay + '</h6>\n«' + esc(fn) + '»', function () {
// chrome 91 may permanently taint on a failed play()
// depending on win10 settings or something? idk
mp.au = null;
mp.au = mpo.au = null;
play(tid, true, seek);
mp.fade_in();
@@ -3234,7 +3286,9 @@ var fileman = (function () {
if (r.clip === null)
r.clip = jread('fman_clip', []).slice(1);
var nsel = msel.getsel().length;
var sel = msel.getsel(),
nsel = sel.length;
clmod(bren, 'en', nsel);
clmod(bdel, 'en', nsel);
clmod(bcut, 'en', nsel);
@@ -3246,9 +3300,51 @@ var fileman = (function () {
clmod(bpst, 'hide', !(have_mv && has(perms, 'write')));
clmod(ebi('wfm'), 'act', QS('#wfm a.en:not(.hide)'));
var wfs = ebi('wfs'), h = '';
try {
wfs.innerHTML = h = r.fsi(sel);
}
catch (ex) { }
clmod(wfs, 'act', h);
bpst.setAttribute('tt', L.ft_paste.format(r.clip.length));
};
r.fsi = function (sel) {
if (!sel.length)
return '';
var lf = treectl.lsc.files,
nf = 0,
sz = 0,
dur = 0,
ntab = new Set();
for (var a = 0; a < sel.length; a++)
ntab.add(sel[a].vp.split('/').pop());
for (var a = 0; a < lf.length; a++) {
if (!ntab.has(lf[a].href.split('?')[0]))
continue;
var f = lf[a];
nf++;
sz += f.sz;
if (f.tags && f.tags['.dur'])
dur += f.tags['.dur']
}
if (!nf)
return '';
var ret = '{0}<br />{1}<small>F</small>'.format(humansize(sz), nf);
if (dur)
ret += ' ' + s2ms(dur);
return ret;
};
r.rename = function (e) {
ev(e);
if (clgot(bren, 'hide'))
@@ -6975,6 +7071,7 @@ function sandbox(tgt, rules, cls, html) {
'},1)</script></body></html>';
var fr = mknod('iframe');
fr.setAttribute('title', 'folder ' + tid + 'logue');
fr.setAttribute('sandbox', rules ? 'allow-' + rules.replace(/ /g, ' allow-') : '');
fr.setAttribute('srcdoc', html);
tgt.innerHTML = '';
@@ -7277,21 +7374,25 @@ ebi('files').onclick = ebi('docul').onclick = function (e) {
function reload_mp() {
if (mp && mp.au) {
if (afilt)
afilt.stop();
mp.au.pause();
mp.au = null;
mpo.au = mp.au;
mpo.au2 = mp.au2;
mpo.acs = mp.acs;
mpl.unbuffer();
}
mpl.stop();
var plays = QSA('tr>td:first-child>a.play');
for (var a = plays.length - 1; a >= 0; a--)
plays[a].parentNode.innerHTML = '-';
mp = new MPlayer();
if (afilt)
afilt.acst = {};
if (mp.au && mp.au.tid) {
var el = QS('a#a' + mp.au.tid);
if (el)
clmod(el, 'act', 1);
el = el && el.closest('tr');
if (el)
clmod(el, 'play', 1);
}
setTimeout(pbar.onresize, 1);
}

View File

@@ -181,7 +181,7 @@
<p><em>note: if you are on LAN (or just dont have valid certificates), add <code>-td</code></em></p>
{% endif %}
<p>
you can use <a href="{{ r }}/.cpr/a/up2k.py">up2k.py</a> to upload (sometimes faster than web-browsers)
you can use <a href="{{ r }}/.cpr/a/u2c.py">u2c.py</a> to upload (sometimes faster than web-browsers)
</p>

View File

@@ -1826,6 +1826,7 @@ function up2k_init(subtle) {
timer.rm(etafun);
timer.rm(donut.do);
ebi('u2tabw').style.minHeight = '0px';
utw_minh = 0;
}

View File

@@ -742,7 +742,7 @@ function get_pwd() {
if (pwd.length < 2)
return null;
return pwd[1].split(';')[0];
return decodeURIComponent(pwd[1].split(';')[0]);
}
@@ -1769,7 +1769,6 @@ function cprop(name) {
function bchrome() {
console.log(document.documentElement.className);
var v, o = QS('meta[name=theme-color]');
if (!o)
return;

View File

@@ -1,3 +1,109 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2023-0507-1834 `v1.7.1` CräzY;PWDs
## new features
* webdav:
* support write-only folders
* option `--dav-auth` / volflag `davauth` forces clients to always auth
* helps clients such as `davfs2` see all folders if the root is anon-readable but some subfolders are not
* alternatively you could configure your client to always send the password in the `PW` header
* include usernames in http request logs
* audio player:
* consumes less power on phones when the screen is off
* smoother playback cursor on short songs
## bugfixes
* the characters `;` and `%` can now be used in passwords
* but non-ascii characters (such as the ä in the release title) can, in fact, not
* verify that all accounts have unique passwords on startup (#25)
## other changes
* ftpd: log incorrect passwords only, not correct ones
* `up2k.py` (the upload, folder-sync, and file-search client) has been renamed to [u2c.py](https://github.com/9001/copyparty/tree/hovudstraum/bin#u2cpy)
* `u2c` as in `up2k client`, or `up2k CLI`, or `upload-to-copyparty` -- good name
* now the only things named "up2k" are the web-ui and the server backend which is way less confusing
* upgrade packaging from [setup.py](https://github.com/9001/copyparty/blob/hovudstraum/setup.py) to [pyproject.toml](https://github.com/9001/copyparty/blob/hovudstraum/pyproject.toml)
* no practical consequences aside from a warm fuzzy feeling of being in the future
* the docker images ~~will be~~ got rebuilt 2023-05-11 ~~in a few days (when [alpine](https://alpinelinux.org/) 3.18 is released)~~ enabling [the chiptune player](https://a.ocv.me/pub/demo/music/chiptunes/#af-f6fb2e5f)
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2023-0429-2114 `v1.7.0` unlinked
don't get excited! nothing new and revolutionary, but `xvol` and `xdev` changed behavior so there's an above-average chance of fresh bugs
## new features
* (#24): `xvol` and `xdev`, previously just hints to the filesystem indexer, now actively block access as well:
* `xvol` stops users following symlinks leaving the volumes they have access to
* so if you symlink `/home/ed/music` into `/srv/www/music` it'll get blocked
* ...unless both folders are accessible through volumes, and the user has read-access to both
* `xdev` stops users crossing the filesystem boundary of the volumes they have access to
* so if you symlink another HDD into a volume it'll get blocked, but you can still symlink from other places on the same FS
* enabling these will add a slight performance hit; the unlikely worst-case is `14%` slower directory listings, `35%` slower download-as-tar
* file selection summary (num files, size, audio duration) in the bottom right
* [u2cli](https://github.com/9001/copyparty/blob/hovudstraum/bin/up2k.py): more aggressive resolving with `--rh`
* [add a warning](https://github.com/9001/copyparty#fix-unreliable-playback-on-android) that the default powersave settings in android may stop playing music during album changes
* also appears [in the media player](https://user-images.githubusercontent.com/241032/235327191-7aaefff9-5d41-4e42-b71f-042a8247f29d.png) if the issue is detected at runtime (playback halts for 30sec while screen is off)
## bugfixes
* (#23): stop autodeleting empty folders when moving or deleting files
* but files which expire / [self-destruct](https://github.com/9001/copyparty#self-destruct) still clean up parent directories like before
* ftp-server: some clients could fail to `mkdir` at first attempt (and also complain during rmdir)
## other changes
* new version of [cpp-winpe64.exe](https://github.com/9001/copyparty/releases/download/v1.7.0/copyparty-winpe64.exe) since the ftp-server fix might be relevant
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2023-0426-2300 `v1.6.15` unexpected boost
## new features
* 30% faster folder listings due to [the very last thing](https://github.com/9001/copyparty/commit/55c74ad164633a0a64dceb51f7f534da0422cbb5) i'd ever expect to be a bottleneck, [thx perf](https://docs.python.org/3.12/howto/perf_profiling.html)
* option to see the lastmod timestamps of symlinks instead of the target files
* makes the turbo mode of [u2cli, the commandline uploader and folder-sync tool](https://github.com/9001/copyparty/blob/hovudstraum/bin/up2k.py) more turbo since copyparty dedupes uploads by symlinking to an existing copy and the symlink is stamped with the deduped file's lastmod
* **webdav:** enabled by default (because rclone will want this), can be disabled with arg `--dav-rt` or volflag `davrt`
* **http:** disabled by default, can be enabled per-request with urlparam `lt`
* [u2cli](https://github.com/9001/copyparty/blob/hovudstraum/bin/up2k.py): option `--rh` to resolve server hostname only once at start of upload
* fantastic for buggy networks, but it'll break TLS
## bugfixes
* new arg `--s-tbody` specifies the network timeout before a dead connection gets dropped (default 3min)
* before there was no timeout at all, which could hang uploads or possibly consume all server resources
* ...but this is only relevant if your copyparty is directly exposed to the internet with no reverse proxy
* with nginx/caddy/etc you can disable the timeout with `--s-tbody 0` for a 3% performance boost (*wow!*)
* iPhone audio transcoder could turn bad and stop transcoding
* ~~maybe android phones no longer pause playback at the end of an album~~
* nope, that was due to [android's powersaver](https://github.com/9001/copyparty#fix-unreliable-playback-on-android), oh well
* ***bonus unintended feature:*** navigate into other folders while a song is plaing
* [installing from the source tarball](https://github.com/9001/copyparty/blob/hovudstraum/docs/devnotes.md#build-from-release-tarball) should be ok now
* good base for making distro packages probably
## other changes
* since the network timeout fix is relevant for the single usecase that [cpp-winpe64.exe](https://github.com/9001/copyparty/releases/download/v1.6.15/copyparty-winpe64.exe) covers, there is now a new version of that
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2023-0424-0609 `v1.6.14` unsettable flags
## new features
* unset a volflag (override a global option) by negating it (setting volflag `-flagname`)
* new argument `--cert` to specify TLS certificate location
* defaults to `~/.config/copyparty/cert.pem` like before
## bugfixes
* in zip/tar downloads, always use the parent-folder name as the archive root
* more reliable ftp authentication when providing password as username
* connect-page: fix rclone ftps example
## other changes
* stop suggesting `--http-only` and `--https-only` for performance since the difference is negligible
* mention how some antivirus (avast, avg, mcafee) thinks that pillow's webp encoder is a virus, affecting `copyparty.exe`
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2023-0420-2141 `v1.6.12` as seen on nixos

View File

@@ -17,6 +17,7 @@
* [building](#building)
* [dev env setup](#dev-env-setup)
* [just the sfx](#just-the-sfx)
* [build from release tarball](#build-from-release-tarball) - uses the included prebuilt webdeps
* [complete release](#complete-release)
* [todo](#todo) - roughly sorted by priority
* [discarded ideas](#discarded-ideas)
@@ -84,7 +85,7 @@ as a result, the hashes are much less useful than they could have been (search t
however it allows for hashing multiple chunks in parallel, greatly increasing upload speed from fast storage (NVMe, raid-0 and such)
* both the [browser uploader](https://github.com/9001/copyparty#uploading) and the [commandline one](https://github.com/9001/copyparty/blob/hovudstraum/bin/up2k.py) does this now, allowing for fast uploading even from plaintext http
* both the [browser uploader](https://github.com/9001/copyparty#uploading) and the [commandline one](https://github.com/9001/copyparty/tree/hovudstraum/bin#u2cpy) does this now, allowing for fast uploading even from plaintext http
hashwasm would solve the streaming issue but reduces hashing speed for sha512 (xxh128 does 6 GiB/s), and it would make old browsers and [iphones](https://bugs.webkit.org/show_bug.cgi?id=228552) unsupported
@@ -110,6 +111,7 @@ authenticate using header `Cookie: cppwd=foo` or url param `&pw=foo`
| 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 | `?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 |
| GET | `?tree` | list one level of subdirectories for each level until URL |
@@ -226,6 +228,8 @@ pip install mutagen # audio metadata
pip install pyftpdlib # ftp server
pip install impacket # smb server -- disable Windows Defender if you REALLY need this on windows
pip install Pillow pyheif-pillow-opener pillow-avif-plugin # thumbnails
pip install pyvips # faster thumbnails
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
```
@@ -255,10 +259,34 @@ then build the sfx using any of the following examples:
```
## build from release tarball
uses the included prebuilt webdeps
if you downloaded a [release](https://github.com/9001/copyparty/releases) source tarball from github (for example [copyparty-1.6.15.tar.gz](https://github.com/9001/copyparty/releases/download/v1.6.15/copyparty-1.6.15.tar.gz) so not the autogenerated one) you can build it like so,
```bash
python3 -m pip install --user -U build setuptools wheel jinja2 strip_hints
bash scripts/run-tests.sh python3 # optional
python3 -m build
```
if you are unable to use `build`, you can use the old setuptools approach instead,
```bash
python3 setup.py install --user setuptools wheel jinja2
python3 setup.py build
# you now have a wheel which you can install. or extract and repackage:
python3 setup.py install --skip-build --prefix=/usr --root=$HOME/pe/copyparty
```
## complete release
also builds the sfx so skip the sfx section above
does everything completely from scratch, straight from your local repo
in the `scripts` folder:
* run `make -C deps-docker` to build all dependencies

View File

@@ -194,6 +194,9 @@ sqlite3 .hist/up2k.db 'select * from mt where k="fgsfds" or k="t:mtp"' | tee /de
for ((f=420;f<1200;f++)); do sz=$(ffmpeg -y -f lavfi -i sine=frequency=$f:duration=2 -vf volume=0.1 -ac 1 -ar 44100 -f s16le /dev/shm/a.wav 2>/dev/null; base64 -w0 </dev/shm/a.wav | gzip -c | wc -c); printf '%d %d\n' $f $sz; done | tee /dev/stderr | sort -nrk2,2
ffmpeg -y -f lavfi -i sine=frequency=1050:duration=2 -vf volume=0.1 -ac 1 -ar 44100 /dev/shm/a.wav
# better sine
sox -DnV -r8000 -b8 -c1 /dev/shm/a.wav synth 1.1 sin 400 vol 0.02
# play icon calibration pics
for w in 150 170 190 210 230 250; do for h in 130 150 170 190 210; do /c/Program\ Files/ImageMagick-7.0.11-Q16-HDRI/magick.exe convert -size ${w}x${h} xc:brown -fill orange -draw "circle $((w/2)),$((h/2)) $((w/2)),$((h/3))" $w-$h.png; done; done

View File

@@ -72,7 +72,7 @@ rclone.exe mount --vfs-cache-mode writes --vfs-cache-max-age 5s --attr-timeout 5
# sync folders to/from copyparty
note that the up2k client [up2k.py](https://github.com/9001/copyparty/tree/hovudstraum/bin#up2kpy) (available on the "connect" page of your copyparty server) does uploads much faster and safer, but rclone is bidirectional and more ubiquitous
note that the up2k client [u2c.py](https://github.com/9001/copyparty/tree/hovudstraum/bin#u2cpy) (available on the "connect" page of your copyparty server) does uploads much faster and safer, but rclone is bidirectional and more ubiquitous
```
rclone sync /usr/share/icons/ cpp-rw:fds/

View File

@@ -287,7 +287,7 @@ symbol legend,
* `curl-friendly ls` = returns a [sortable plaintext folder listing](https://user-images.githubusercontent.com/241032/215322619-ea5fd606-3654-40ad-94ee-2bc058647bb2.png) when curled
* `curl-friendly upload` = uploading with curl is just `curl -T some.bin http://.../`
* `a`/copyparty remarks:
* one-way folder sync from local to server can be done efficiently with [up2k.py](https://github.com/9001/copyparty/blob/hovudstraum/bin/up2k.py), or with webdav and conventional rsync
* one-way folder sync from local to server can be done efficiently with [u2c.py](https://github.com/9001/copyparty/tree/hovudstraum/bin#u2cpy), or with webdav and conventional rsync
* can hot-reload config files (with just a few exceptions)
* can set per-folder permissions if that folder is made into a separate volume, so there is configuration overhead
* [event hooks](https://github.com/9001/copyparty/tree/hovudstraum/bin/hooks) ([discord](https://user-images.githubusercontent.com/241032/215304439-1c1cb3c8-ec6f-4c17-9f27-81f969b1811a.png), [desktop](https://user-images.githubusercontent.com/241032/215335767-9c91ed24-d36e-4b6b-9766-fb95d12d163f.png)) inspired by filebrowser, as well as the more complex [media parser](https://github.com/9001/copyparty/tree/hovudstraum/bin/mtag) alternative

146
pyproject.toml Normal file
View File

@@ -0,0 +1,146 @@
[project]
name = "copyparty"
description = """
Portable file server with accelerated resumable uploads, \
deduplication, WebDAV, FTP, zeroconf, media indexer, \
video thumbnails, audio transcoding, and write-only folders"""
readme = "README.md"
authors = [{ name = "ed", email = "copyparty@ocv.me" }]
license = { text = "MIT" }
requires-python = ">=3.3"
dependencies = ["Jinja2"]
dynamic = ["version"]
classifiers = [
"Development Status :: 5 - Production/Stable",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.3",
"Programming Language :: Python :: 3.4",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: Jython",
"Programming Language :: Python :: Implementation :: PyPy",
"Environment :: Console",
"Environment :: No Input/Output (Daemon)",
"Intended Audience :: End Users/Desktop",
"Intended Audience :: System Administrators",
"Topic :: Communications :: File Sharing",
"Topic :: Internet :: File Transfer Protocol (FTP)",
"Topic :: Internet :: WWW/HTTP :: HTTP Servers",
]
[project.urls]
"Source Code" = "https://github.com/9001/copyparty"
"Bug Tracker" = "https://github.com/9001/copyparty/issues"
"Demo Server" = "https://a.ocv.me/pub/demo/"
[project.optional-dependencies]
thumbnails = ["Pillow"]
thumbnails2 = ["pyvips"]
audiotags = ["mutagen"]
ftpd = ["pyftpdlib"]
ftps = ["pyftpdlib", "pyopenssl"]
[project.scripts]
copyparty = "copyparty.__main__:main"
"u2c" = "copyparty.web.a.u2c:main"
"partyfuse" = "copyparty.web.a.partyfuse:main"
# =====================================================================
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"
# requires = ["hatchling"]
# build-backend = "hatchling.build"
[tool.hatch.version]
source = "code"
path = "copyparty/__version__.py"
[tool.setuptools.dynamic]
version = { attr = "copyparty.__version__.__version__" }
[tool.setuptools.packages.find]
include = ["copyparty*"]
[tool.setuptools.package-data]
copyparty = [
"res/COPYING.txt",
"res/insecure.pem",
"web/*.gz",
"web/*.js",
"web/*.css",
"web/*.html",
"web/a/*.bat",
"web/dd/*.png",
"web/deps/*.gz",
"web/deps/*.woff*",
]
# =====================================================================
[tool.black]
required-version = '21.12b0'
target-version = ['py27']
[tool.isort]
profile = "black"
include_trailing_comma = true
force_sort_within_sections = true
[tool.bandit]
skips = ["B104", "B110", "B112"]
# =====================================================================
[tool.pylint.MAIN]
py-version = "3.11"
jobs = 2
[tool.pylint."MESSAGES CONTROL"]
disable = [
"missing-module-docstring",
"missing-class-docstring",
"missing-function-docstring",
"import-outside-toplevel",
"wrong-import-position",
"raise-missing-from",
"bare-except",
"broad-exception-raised",
"broad-exception-caught",
"invalid-name",
"line-too-long",
"too-many-lines",
"consider-using-f-string",
"pointless-string-statement",
]
[tool.pylint.FORMAT]
expected-line-ending-format = "LF"
# =====================================================================
[tool.mypy]
python_version = "3.11"
files = ["copyparty"]
show_error_codes = true
show_column_numbers = true
pretty = true
strict = true
local_partial_types = true
strict_equality = true
warn_unreachable = true
ignore_missing_imports = true
follow_imports = "silent"
[[tool.mypy.overrides]]
no_implicit_reexport = false

View File

@@ -34,7 +34,7 @@ set -e
# 4823 copyparty-extras/copyparty-repack.sh
# `- source files from github
#
# 23663 copyparty-extras/up2k.py
# 23663 copyparty-extras/u2c.py
# `- standalone utility to upload or search for files
#
# 32280 copyparty-extras/partyfuse.py
@@ -147,7 +147,7 @@ repack sfx-lite "re no-dd no-cm no-hl gz"
# copy lite-sfx.py to ./copyparty,
# delete extracted source code
( cd copyparty-extras/
mv copyparty-*/bin/up2k.py .
mv copyparty-*/bin/u2c.py .
mv copyparty-*/bin/partyfuse.py .
cp -pv sfx-lite/copyparty-sfx.py ../copyparty
rm -rf copyparty-{0..9}*.*.*{0..9}

View File

@@ -1,5 +1,4 @@
# TODO easymde embeds codemirror on 3.17 due to new npm probably
FROM alpine:3.16
FROM alpine:3.18
WORKDIR /z
ENV ver_asmcrypto=c72492f4a66e17a0e5dd8ad7874de354f3ccdaa5 \
ver_hashwasm=4.9.0 \

View File

@@ -5,11 +5,13 @@ LABEL org.opencontainers.image.url="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)"
ENV PYTHONPYCACHEPREFIX=/tmp/pyc
RUN apk --no-cache add \
RUN apk --no-cache add !pyc \
wget \
py3-pillow \
ffmpeg \
&& rm -rf /tmp/pyc \
&& mkdir /cfg /w \
&& chmod 777 /cfg /w \
&& echo % /cfg > initcfg

View File

@@ -5,26 +5,27 @@ LABEL org.opencontainers.image.url="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"
ENV PYTHONPYCACHEPREFIX=/tmp/pyc
COPY i/bin/mtag/install-deps.sh ./
COPY i/bin/mtag/audio-bpm.py /mtag/
COPY i/bin/mtag/audio-key.py /mtag/
RUN apk add -U \
RUN apk add -U !pyc \
wget \
py3-pillow py3-pip py3-cffi \
ffmpeg \
vips-jxl vips-heif vips-poppler vips-magick \
py3-numpy fftw libsndfile \
vamp-sdk vamp-sdk-libs \
&& python3 -m pip install pyvips \
&& apk --no-cache add -t .bd \
&& apk add -t .bd \
bash wget gcc g++ make cmake patchelf \
python3-dev ffmpeg-dev fftw-dev libsndfile-dev \
py3-wheel py3-numpy-dev \
vamp-sdk-dev \
&& python3 -m pip install pyvips \
&& bash install-deps.sh \
&& apk del py3-pip .bd \
&& rm -rf /var/cache/apk/* \
&& rm -rf /var/cache/apk/* /tmp/pyc \
&& chmod 777 /root \
&& ln -s /root/vamp /root/.local / \
&& mkdir /cfg /w \

View File

@@ -5,10 +5,12 @@ LABEL org.opencontainers.image.url="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)"
ENV PYTHONPYCACHEPREFIX=/tmp/pyc
RUN apk --no-cache add \
RUN apk --no-cache add !pyc \
wget \
py3-pillow py3-mutagen \
&& rm -rf /tmp/pyc \
&& mkdir /cfg /w \
&& chmod 777 /cfg /w \
&& echo % /cfg > initcfg

View File

@@ -5,14 +5,19 @@ LABEL org.opencontainers.image.url="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)"
ENV PYTHONPYCACHEPREFIX=/tmp/pyc
RUN apk --no-cache add \
RUN apk add -U !pyc \
wget \
py3-pillow py3-pip py3-cffi \
ffmpeg \
vips-jxl vips-heif vips-poppler vips-magick \
&& apk add -t .bd \
bash wget gcc g++ make cmake patchelf \
python3-dev py3-wheel \
&& python3 -m pip install pyvips \
&& apk del py3-pip \
&& apk del py3-pip .bd \
&& rm -rf /var/cache/apk/* /tmp/pyc \
&& mkdir /cfg /w \
&& chmod 777 /cfg /w \
&& echo % /cfg > initcfg

View File

@@ -5,9 +5,11 @@ LABEL org.opencontainers.image.url="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"
ENV PYTHONPYCACHEPREFIX=/tmp/pyc
RUN apk --no-cache add \
RUN apk --no-cache add !pyc \
python3 \
&& rm -rf /tmp/pyc \
&& mkdir /cfg /w \
&& chmod 777 /cfg /w \
&& echo % /cfg > initcfg

View File

@@ -5,10 +5,12 @@ LABEL org.opencontainers.image.url="https://github.com/9001/copyparty" \
org.opencontainers.image.licenses="MIT" \
org.opencontainers.image.title="copyparty-min-pip" \
org.opencontainers.image.description="just copyparty, no thumbnails, no media tags, no audio transcoding"
ENV PYTHONPYCACHEPREFIX=/tmp/pyc
RUN apk --no-cache add python3 py3-pip \
RUN apk --no-cache add python3 py3-pip !pyc \
&& python3 -m pip install copyparty \
&& apk del py3-pip \
&& rm -rf /tmp/pyc \
&& mkdir /cfg /w \
&& chmod 777 /cfg /w \
&& echo % /cfg > initcfg

View File

@@ -95,7 +95,7 @@ filt=
[ $(jobs -p | wc -l) -lt $(nproc) ] && break
while [ -e .blk ]; do sleep 0.2; done
done
aa="$(printf '%7s' $a)"
aa="$(printf '%11s' $a-$i)"
# arm takes forever so make it top priority
[ ${a::3} == arm ] && nice= || nice=nice

View File

@@ -73,14 +73,17 @@ pydir="$(
}
function have() {
python -c "import $1; $1; $1.__version__"
python -c "import $1; $1; getattr($1,'__version__',0)"
}
function load_env() {
. buildenv/bin/activate
have setuptools
have wheel
have build
have twine
have jinja2
have strip_hints
}
load_env || {
@@ -88,19 +91,32 @@ load_env || {
deactivate || true
rm -rf buildenv
python3 -m venv buildenv
(. buildenv/bin/activate && pip install twine wheel)
(. buildenv/bin/activate && pip install \
setuptools wheel build twine jinja2 strip_hints )
load_env
}
# cleanup
rm -rf unt build/pypi
# grab licenses
scripts/genlic.sh copyparty/res/COPYING.txt
# remove type hints to support python < 3.9
# clean-ish packaging env
rm -rf build/pypi
mkdir -p build/pypi
cp -pR setup.py README.md LICENSE copyparty contrib bin scripts/strip_hints build/pypi/
cp -pR pyproject.toml README.md LICENSE copyparty contrib bin scripts/strip_hints build/pypi/
tar -c docs/lics.txt scripts/genlic.sh build/*.txt | tar -xC build/pypi/
cd build/pypi
# delete junk
find -name '*.pyc' -delete
find -name __pycache__ -delete
find -name py.typed -delete
find -type f \( -name .DS_Store -or -name ._.DS_Store \) -delete
find -type f -name ._\* | while IFS= read -r f; do cmp <(printf '\x00\x05\x16') <(head -c 3 -- "$f") && rm -f -- "$f"; done
# remove type hints to support python < 3.9
f=../strip-hints-0.1.10.tar.gz
[ -e $f ] ||
(url=https://files.pythonhosted.org/packages/9c/d4/312ddce71ee10f7e0ab762afc027e07a918f1c0e1be5b0069db5b0e7542d/strip-hints-0.1.10.tar.gz;
@@ -132,24 +148,13 @@ while IFS= read -r x; do
done
rm -rf contrib
[ $fast ] && sed -ri s/5730/10/ copyparty/web/Makefile
[ $fast ] && sed -ri s/573/10/ copyparty/web/Makefile
(cd copyparty/web && make -j$(nproc) && rm Makefile)
# build
./setup.py clean2
./setup.py sdist bdist_wheel --universal
python3 -m build
[ "$mode" == t ] && twine upload -r pypitest dist/*
[ "$mode" == u ] && twine upload -r pypi dist/*
cat <<EOF
all done!
to clean up the source tree:
cd ~/dev/copyparty
./setup.py clean2
EOF
true

View File

@@ -64,6 +64,8 @@ git archive hovudstraum | tar -xC "$rls_dir"
echo ">>> export untracked deps"
tar -c copyparty/web/deps | tar -xC "$rls_dir"
scripts/genlic.sh "$rls_dir/copyparty/res/COPYING.txt"
cd "$rls_dir"
find -type d -exec chmod 755 '{}' \+
find -type f -exec chmod 644 '{}' \+
@@ -93,7 +95,8 @@ rm \
.gitattributes \
.gitignore
mv LICENSE LICENSE.txt
cp -pv LICENSE LICENSE.txt
mv setup.py{,.disabled}
# the regular cleanup memes
find -name '*.pyc' -delete

View File

@@ -11,30 +11,12 @@ update_arch_pkgbuild() {
rm -rf x
mkdir x
(echo "$self/../dist/copyparty-sfx.py"
awk -v self="$self" '
/^\)/{o=0}
/^source=/{o=1;next}
{
sub(/..pkgname./,"copyparty");
sub(/.*pkgver./,self "/..");
sub(/^ +"/,"");sub(/"/,"")
}
o&&!/https/' PKGBUILD
) |
xargs sha256sum > x/sums
sha=$(sha256sum "$self/../dist/copyparty-$ver.tar.gz" | awk '{print$1}')
(awk -v ver=$ver '
awk -v ver=$ver -v sha=$sha '
/^pkgver=/{sub(/[0-9\.]+/,ver)};
/^sha256sums=/{exit};
1' PKGBUILD
echo -n 'sha256sums=('
p=; cat x/sums | while read s _; do
echo "$p\"$s\""
p=' '
done
awk '/^sha256sums=/{o=1} o&&/^\)/{o=2} o==2' PKGBUILD
) >a
/^sha256sums=/{sub(/[0-9a-f]{64}/,sha)};
1' PKGBUILD >a
mv a PKGBUILD
rm -rf x

View File

@@ -7,7 +7,7 @@ fe62705893c86eeb2d5b841da8debe05dedda98364dec190b487e718caad8a8735503bf93739a7a2
132a5380f33a245f2e744413a0e1090bc42b7356376de5121397cec5976b04b79f7c9ebe28af222c9c7b01461f7d7920810d220e337694727e0d7cd9e91fa667 pywin32_ctypes-0.2.0-py2.py3-none-any.whl
3c5adf0a36516d284a2ede363051edc1bcc9df925c5a8a9fa2e03cab579dd8d847fdad42f7fd5ba35992e08234c97d2dbfec40a9d12eec61c8dc03758f2bd88e typing_extensions-4.4.0-py3-none-any.whl
4b6e9ae967a769fe32be8cf0bc0d5a213b138d1e0344e97656d08a3d15578d81c06c45b334c872009db2db8f39db0c77c94ff6c35168d5e13801917667c08678 upx-4.0.2-win32.zip
# up2k (win7)
# u2c (win7)
a7d259277af4948bf960682bc9fb45a44b9ae9a19763c8a7c313cef4aa9ec2d447d843e4a7c409e9312c8c8f863a24487a8ee4ffa6891e9b1c4e111bb4723861 certifi-2022.12.7-py3-none-any.whl
2822c0dae180b1c8cfb7a70c8c00bad62af9afdbb18b656236680def9d3f1fcdcb8ef5eb64fc3b4c934385cd175ad5992a2284bcba78a243130de75b2d1650db charset_normalizer-3.1.0-cp37-cp37m-win32.whl
ffdd45326f4e91c02714f7a944cbcc2fdd09299f709cfa8aec0892053eef0134fb80d9ba3790afd319538a86feb619037cbf533e2f5939cb56b35bb17f56c858 idna-3.4-py3-none-any.whl

View File

@@ -13,7 +13,7 @@ https://pypi.org/project/MarkupSafe/#files
https://pypi.org/project/mutagen/#files
https://pypi.org/project/Pillow/#files
# up2k (win7) additionals
# u2c (win7) additionals
https://pypi.org/project/certifi/#files
https://pypi.org/project/charset-normalizer/#files # cp37-cp37m-win32.whl
https://pypi.org/project/idna/#files

View File

@@ -18,9 +18,9 @@ VSVersionInfo(
[StringStruct('CompanyName', 'ocv.me'),
StringStruct('FileDescription', 'copyparty uploader / filesearch command'),
StringStruct('FileVersion', '1.2.3'),
StringStruct('InternalName', 'up2k'),
StringStruct('InternalName', 'u2c'),
StringStruct('LegalCopyright', '2019, ed'),
StringStruct('OriginalFilename', 'up2k.exe'),
StringStruct('OriginalFilename', 'u2c.exe'),
StringStruct('ProductName', 'copyparty up2k client'),
StringStruct('ProductVersion', '1.2.3')])
]),

View File

@@ -14,7 +14,7 @@ uname -s | grep -E 'WOW64|NT-10' && echo need win7-32 && exit 1
dl() { curl -fkLO "$1"; }
cd ~/Downloads
dl https://192.168.123.1:3923/cpp/bin/up2k.py
dl https://192.168.123.1:3923/cpp/bin/u2c.py
dl https://192.168.123.1:3923/cpp/scripts/pyinstaller/up2k.ico
dl https://192.168.123.1:3923/cpp/scripts/pyinstaller/up2k.rc
dl https://192.168.123.1:3923/cpp/scripts/pyinstaller/up2k.spec
@@ -37,12 +37,12 @@ grep -E '^from .ssl_ import' $APPDATA/python/python37/site-packages/urllib3/util
echo golfed
}
read a b _ < <(awk -F\" '/^S_VERSION =/{$0=$2;sub(/\./," ");print}' < up2k.py)
read a b _ < <(awk -F\" '/^S_VERSION =/{$0=$2;sub(/\./," ");print}' < u2c.py)
sed -r 's/1,2,3,0/'$a,$b,0,0'/;s/1\.2\.3/'$a.$b.0/ <up2k.rc >up2k.rc2
#python uncomment.py up2k.py
#python uncomment.py u2c.py
$APPDATA/python/python37/scripts/pyinstaller -y --clean --upx-dir=. up2k.spec
./dist/up2k.exe --version
./dist/u2c.exe --version
curl -fkT dist/up2k.exe -HPW:wark https://192.168.123.1:3923/
curl -fkT dist/u2c.exe -HPW:wark https://192.168.123.1:3923/

View File

@@ -5,7 +5,7 @@ block_cipher = None
a = Analysis(
['up2k.py'],
['u2c.py'],
pathex=[],
binaries=[],
datas=[],
@@ -60,7 +60,7 @@ exe = EXE(
a.zipfiles,
a.datas,
[],
name='up2k',
name='u2c',
debug=False,
bootloader_ignore_signals=False,
strip=False,

View File

@@ -11,4 +11,4 @@ ex=(
encodings.{zlib_codec,base64_codec,bz2_codec,charmap,hex_codec,palmos,punycode,rot_13}
);
cex=(); for a in "${ex[@]}"; do cex+=(--exclude "$a"); done
$APPDATA/python/python37/scripts/pyi-makespec --version-file up2k.rc2 -i up2k.ico -n up2k -c -F up2k.py "${cex[@]}"
$APPDATA/python/python37/scripts/pyi-makespec --version-file up2k.rc2 -i up2k.ico -n u2c -c -F u2c.py "${cex[@]}"

View File

@@ -1,7 +1,7 @@
#!/bin/bash
set -e
parallel=2
parallel=1
[ -e make-sfx.sh ] || cd scripts
[ -e make-sfx.sh ] && [ -e deps-docker ] || {

View File

@@ -23,6 +23,7 @@ copyparty/mdns.py,
copyparty/mtag.py,
copyparty/multicast.py,
copyparty/res,
copyparty/res/__init__.py,
copyparty/res/COPYING.txt,
copyparty/res/insecure.pem,
copyparty/smbd.py,
@@ -62,7 +63,7 @@ copyparty/web,
copyparty/web/a,
copyparty/web/a/__init__.py,
copyparty/web/a/partyfuse.py,
copyparty/web/a/up2k.py,
copyparty/web/a/u2c.py,
copyparty/web/a/webdav-cfg.bat,
copyparty/web/baguettebox.js,
copyparty/web/browser.css,

View File

@@ -7,6 +7,11 @@ import sys
from shutil import rmtree
from setuptools import setup, Command
_ = """
this probably still works but is no longer in use;
pyproject.toml and scripts/make-pypi-release.sh
are in charge of packaging wheels now
"""
NAME = "copyparty"
VERSION = None
@@ -137,7 +142,7 @@ args = {
"ftps": ["pyftpdlib", "pyopenssl"],
},
"entry_points": {"console_scripts": ["copyparty = copyparty.__main__:main"]},
"scripts": ["bin/partyfuse.py", "bin/up2k.py"],
"scripts": ["bin/partyfuse.py", "bin/u2c.py"],
"cmdclass": {"clean2": clean2},
}

View File

@@ -98,7 +98,7 @@ class Cfg(Namespace):
def __init__(self, a=None, v=None, c=None):
ka = {}
ex = "daw dav_inf dav_mac dotsrch e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp ed emp force_js getmod hardlink ih ihead magic never_symlink nid nih no_acode no_athumb no_dav no_dedup no_del no_dupe no_logues no_mv no_readme no_robots no_sb_md no_sb_lg no_scandir no_thumb no_vthumb no_zip nrand nw rand vc xdev xlink xvol"
ex = "daw dav_auth dav_inf dav_mac dav_rt dotsrch e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp ed emp force_js getmod hardlink ih ihead magic never_symlink nid nih no_acode no_athumb no_dav no_dedup no_del no_dupe no_logues no_mv no_readme no_robots no_sb_md no_sb_lg no_scandir no_thumb no_vthumb no_zip nrand nw rand vc xdev xlink xvol"
ka.update(**{k: False for k in ex.split()})
ex = "dotpart no_rescan no_sendfile no_voldump plain_ip"
@@ -107,6 +107,9 @@ class Cfg(Namespace):
ex = "css_browser hist js_browser no_forget no_hash no_idx"
ka.update(**{k: None for k in ex.split()})
ex = "s_thead s_tbody"
ka.update(**{k: 9 for k in ex.split()})
ex = "df loris re_maxage rproxy rsp_jtr rsp_slp s_wr_slp theme themes turbo"
ka.update(**{k: 0 for k in ex.split()})