Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd40adccdb | ||
|
|
0f2c623599 | ||
|
|
4adbe1b517 | ||
|
|
4f013f64fe | ||
|
|
a9d1310296 | ||
|
|
43e6da3454 | ||
|
|
542a1de1ba | ||
|
|
03d23daecb | ||
|
|
cb019afecf | ||
|
|
5b98e104f2 | ||
|
|
df9feabcf8 | ||
|
|
674fc1fe08 | ||
|
|
a2601fd6ad | ||
|
|
025942a7d6 | ||
|
|
510100c86b | ||
|
|
161bbc7d26 | ||
|
|
7c9c962b79 | ||
|
|
cbdbaf1938 | ||
|
|
cdfceb483e | ||
|
|
2228f81f94 | ||
|
|
895880aeb0 | ||
|
|
6bb27e6091 | ||
|
|
d197e754b9 | ||
|
|
b0dec83aad | ||
|
|
e2c2dd18cf | ||
|
|
ca6d0b8d5e | ||
|
|
48705a74c6 | ||
|
|
b419984709 | ||
|
|
e00b97eee0 | ||
|
|
4dca1cf8f4 | ||
|
|
edba7fffd3 | ||
|
|
21a96bcfe8 | ||
|
|
2d322dd48e | ||
|
|
df6d4df4f8 | ||
|
|
5aa893973c | ||
|
|
be0dd555a6 | ||
|
|
9921c43e3a | ||
|
|
14fa369fae | ||
|
|
0f0f8d90c1 | ||
|
|
1afbff7335 | ||
|
|
8c32b0e7bb | ||
|
|
9bc4c5d2e6 | ||
|
|
1534b7cb55 | ||
|
|
56d3bcf515 | ||
|
|
78605d9a79 | ||
|
|
d46a40fed8 |
21
README.md
21
README.md
@@ -12,7 +12,7 @@ turn almost any device into a file server with resumable uploads/downloads using
|
||||
|
||||
📷 **screenshots:** [browser](#the-browser) // [upload](#uploading) // [unpost](#unpost) // [thumbnails](#thumbnails) // [search](#searching) // [fsearch](#file-search) // [zip-DL](#zip-downloads) // [md-viewer](#markdown-viewer)
|
||||
|
||||
🎬 **videos:** [upload](https://a.ocv.me/pub/demo/pics-vids/up2k.webm) // [cli-upload](https://a.ocv.me/pub/demo/pics-vids/u2cli.webm) // [race-the-beam](https://a.ocv.me/pub/g/nerd-stuff/cpp/2024-0418-race-the-beam.webm)
|
||||
🎬 **videos:** [upload](https://a.ocv.me/pub/demo/pics-vids/up2k.webm) // [cli-upload](https://a.ocv.me/pub/demo/pics-vids/u2cli.webm) // [race-the-beam](https://a.ocv.me/pub/g/nerd-stuff/cpp/2024-0418-race-the-beam.webm) // 👉 **[feature-showcase](https://a.ocv.me/pub/demo/showcase-hq.webm)** ([youtube](https://www.youtube.com/watch?v=15_-hgsX2V0))
|
||||
|
||||
made in Norway 🇳🇴
|
||||
|
||||
@@ -1439,12 +1439,17 @@ if you enable deduplication with `--dedup` then it'll create a symlink instead o
|
||||
**warning:** when enabling dedup, you should also:
|
||||
* enable indexing with `-e2dsa` or volflag `e2dsa` (see [file indexing](#file-indexing) section below); strongly recommended
|
||||
* ...and/or `--hardlink-only` to use hardlink-based deduplication instead of symlinks; see explanation below
|
||||
* ...and/or `--reflink` to use CoW/reflink-based dedup (much safer than hardlink, but OS/FS-dependent)
|
||||
|
||||
it will not be safe to rename/delete files if you only enable dedup and none of the above; if you enable indexing then it is not *necessary* to also do hardlinks (but you may still want to)
|
||||
|
||||
by default, deduplication is done based on symlinks (symbolic links); these are tiny files which are pointers to the nearest full copy of the file
|
||||
|
||||
you can choose to use hardlinks instead of softlinks, globally with `--hardlink-only` or volflag `hardlinkonly`;
|
||||
you can choose to use hardlinks instead of softlinks, globally with `--hardlink-only` or volflag `hardlinkonly`, and you can choose to use reflinks with `--reflink` or volflag `reflink`
|
||||
|
||||
advantages of using reflinks (CoW, copy-on-write):
|
||||
* entirely safe (when your filesystem supports it correctly); either file can be edited or deleted without affecting other copies
|
||||
* only linux 5.3 or newer, only python 3.14 or newer, only some filesystems (btrfs probably ok, maybe xfs too, but zfs had bugs)
|
||||
|
||||
advantages of using hardlinks:
|
||||
* hardlinks are more compatible with other software; they behave entirely like regular files
|
||||
@@ -1605,7 +1610,7 @@ config file example:
|
||||
w: * # anyone can upload here
|
||||
rw: ed # only user "ed" can read-write
|
||||
flags:
|
||||
e2ds: # filesystem indexing is required for many of these:
|
||||
e2ds # filesystem indexing is required for many of these:
|
||||
sz: 1k-3m # accept upload only if filesize in this range
|
||||
df: 4g # free disk space cannot go lower than this
|
||||
vmaxb: 1g # volume can never exceed 1 GiB
|
||||
@@ -1662,6 +1667,8 @@ this can instead be kept in a single place using the `--hist` argument, or the `
|
||||
|
||||
by default, the per-volume `up2k.db` sqlite3-database for `-e2d` and `-e2t` is stored next to the thumbnails according to the `--hist` option, but the global-option `--dbpath` and/or volflag `dbpath` can be used to put the database somewhere else
|
||||
|
||||
if your storage backend is unreliable (NFS or bad HDDs), you can specify one or more "landmarks" to look for before doing anything database-related. A landmark is a file which is always expected to exist inside the volume. This avoids spurious filesystem rescans in the event of an outage. One line per landmark (see example below)
|
||||
|
||||
note:
|
||||
* putting the hist-folders on an SSD is strongly recommended for performance
|
||||
* markdown edits are always stored in a local `.hist` subdirectory
|
||||
@@ -1679,6 +1686,8 @@ config file example:
|
||||
flags:
|
||||
hist: - # restore the default (/mnt/nas/pics/.hist/)
|
||||
hist: /mnt/nas/cache/pics/ # can be absolute path
|
||||
landmark: me.jpg # /mnt/nas/pics/me.jpg must be readable to enable db
|
||||
landmark: info/a.txt^=ok # and this textfile must start with "ok"
|
||||
```
|
||||
|
||||
|
||||
@@ -2018,7 +2027,7 @@ some reverse proxies (such as [Caddy](https://caddyserver.com/)) can automatical
|
||||
* **warning:** nginx-QUIC (HTTP/3) is still experimental and can make uploads much slower, so HTTP/1.1 is recommended for now
|
||||
* depending on server/client, HTTP/1.1 can also be 5x faster than HTTP/2
|
||||
|
||||
for improved security (and a 10% performance boost) consider listening on a unix-socket with `-i unix:770:www:/tmp/party.sock` (permission `770` means only members of group `www` can access it)
|
||||
for improved security (and a 10% performance boost) consider listening on a unix-socket with `-i unix:770:www:/dev/shm/party.sock` (permission `770` means only members of group `www` can access it)
|
||||
|
||||
example webserver / reverse-proxy configs:
|
||||
|
||||
@@ -2239,7 +2248,7 @@ NOTE: there used to be an aur package; this evaporated when copyparty was adopte
|
||||
|
||||
## fedora package
|
||||
|
||||
does not exist yet; using the [copr-pypi](https://copr.fedorainfracloud.org/coprs/g/copr/PyPI/) builds is **NOT recommended** because updates can be delayed by [several months](https://github.com/fedora-copr/copr/issues/3056)
|
||||
does not exist yet; there are rumours that it is being packaged! keep an eye on this space...
|
||||
|
||||
|
||||
## nix package
|
||||
@@ -2364,8 +2373,10 @@ TLDR: yes
|
||||
| send message | yep | yep | yep | yep | yep | yep | yep | yep |
|
||||
| set sort order | - | yep | yep | yep | yep | yep | yep | yep |
|
||||
| zip selection | - | yep | yep | yep | yep | yep | yep | yep |
|
||||
| file search | - | yep | yep | yep | yep | yep | yep | yep |
|
||||
| file rename | - | yep | yep | yep | yep | yep | yep | yep |
|
||||
| file cut/paste | - | yep | yep | yep | yep | yep | yep | yep |
|
||||
| unpost uploads | - | - | yep | yep | yep | yep | yep | yep |
|
||||
| navpane | - | yep | yep | yep | yep | yep | yep | yep |
|
||||
| image viewer | - | yep | yep | yep | yep | yep | yep | yep |
|
||||
| video player | - | yep | yep | yep | yep | yep | yep | yep |
|
||||
|
||||
@@ -85,13 +85,13 @@ server {
|
||||
proxy_buffer_size 16k;
|
||||
proxy_busy_buffers_size 24k;
|
||||
|
||||
proxy_set_header Connection "Keep-Alive";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
# NOTE: with cloudflare you want this instead:
|
||||
#proxy_set_header X-Forwarded-For $http_cf_connecting_ip;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Connection "Keep-Alive";
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
# NOTE: with cloudflare you want this X-Forwarded-For instead:
|
||||
#proxy_set_header X-Forwarded-For $http_cf_connecting_ip;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -279,6 +279,7 @@ in {
|
||||
"/nix/store"
|
||||
"-/etc/resolv.conf"
|
||||
"-/etc/nsswitch.conf"
|
||||
"-/etc/group"
|
||||
"-/etc/hosts"
|
||||
"-/etc/localtime"
|
||||
]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Maintainer: icxes <dev.null@need.moe>
|
||||
pkgname=copyparty
|
||||
pkgver="1.18.1"
|
||||
pkgver="1.18.5"
|
||||
pkgrel=1
|
||||
pkgdesc="File server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++"
|
||||
arch=("any")
|
||||
@@ -22,7 +22,7 @@ optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tag
|
||||
)
|
||||
source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz")
|
||||
backup=("etc/${pkgname}.d/init" )
|
||||
sha256sums=("4f8069987574a580017531ffe7d30bcfc573979cca419f9becebae6b4228525a")
|
||||
sha256sums=("30dd1bbb479187a44f3e44c8322856873c0022485237d457fadfeb5a6af51f7a")
|
||||
|
||||
build() {
|
||||
cd "${srcdir}/${pkgname}-${pkgver}"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"url": "https://github.com/9001/copyparty/releases/download/v1.18.1/copyparty-sfx.py",
|
||||
"version": "1.18.1",
|
||||
"hash": "sha256-Dzz5JXvVH+nCVc8Bc0DV3UdZanzDHRnwGKJYx7N8WZc="
|
||||
"url": "https://github.com/9001/copyparty/releases/download/v1.18.5/copyparty-sfx.py",
|
||||
"version": "1.18.5",
|
||||
"hash": "sha256-rEYjxJwzTzN+upo5UQ8hdYonQiNK1c+SfduS6M/QXw0="
|
||||
}
|
||||
@@ -547,14 +547,15 @@ def get_sects():
|
||||
when running behind a reverse-proxy, it's recommended to
|
||||
use unix-sockets for improved performance and security;
|
||||
|
||||
\033[32m-i unix:770:www:\033[33m/tmp/a.sock\033[0m listens on \033[33m/tmp/a.sock\033[0m with
|
||||
permissions \033[33m0770\033[0m; only accessible to members of the \033[33mwww\033[0m
|
||||
group. This is the best approach. Alternatively,
|
||||
\033[32m-i unix:770:www:\033[33m/dev/shm/party.sock\033[0m listens on
|
||||
\033[33m/dev/shm/party.sock\033[0m with permissions \033[33m0770\033[0m;
|
||||
only accessible to members of the \033[33mwww\033[0m group.
|
||||
This is the best approach. Alternatively,
|
||||
|
||||
\033[32m-i unix:777:\033[33m/tmp/a.sock\033[0m sets perms \033[33m0777\033[0m so anyone can
|
||||
access it; bad unless it's inside a restricted folder
|
||||
\033[32m-i unix:777:\033[33m/dev/shm/party.sock\033[0m sets perms \033[33m0777\033[0m so anyone
|
||||
can access it; bad unless it's inside a restricted folder
|
||||
|
||||
\033[32m-i unix:\033[33m/tmp/a.sock\033[0m keeps umask-defined permissions
|
||||
\033[32m-i unix:\033[33m/dev/shm/party.sock\033[0m keeps umask-defined permission
|
||||
(usually \033[33m0600\033[0m) and the same user/group as copyparty
|
||||
|
||||
\033[33m-p\033[0m (tcp ports) is ignored for unix sockets
|
||||
@@ -863,6 +864,43 @@ def get_sects():
|
||||
"""
|
||||
),
|
||||
],
|
||||
[
|
||||
"chmod",
|
||||
"file/folder permissions",
|
||||
dedent(
|
||||
"""
|
||||
global-option \033[33m--chmod-f\033[0m and volflag \033[33mchmod_f\033[0m specifies the unix-permission to use when creating a new file
|
||||
|
||||
similarly, \033[33m--chmod-d\033[0m and \033[33mchmod_d\033[0m sets the directory/folder perm
|
||||
|
||||
the value is a three-digit octal number such as \033[32m755\033[0m, \033[32m750\033[0m, \033[32m644\033[0m, etc.
|
||||
|
||||
first digit = "User"; permission for the unix-user
|
||||
second digit = "Group"; permission for the unix-group
|
||||
third digit = "Other"; permission for all other users/groups
|
||||
|
||||
for files:
|
||||
\033[32m0\033[0m = \033[35m---\033[0m = no access
|
||||
\033[32m1\033[0m = \033[35m--x\033[0m = can execute the file as a program
|
||||
\033[32m2\033[0m = \033[35m-w-\033[0m = can write
|
||||
\033[32m3\033[0m = \033[35m-wx\033[0m = can write and execute
|
||||
\033[32m4\033[0m = \033[35mr--\033[0m = can read
|
||||
\033[32m5\033[0m = \033[35mr-x\033[0m = can read and execute
|
||||
\033[32m6\033[0m = \033[35mrw-\033[0m = can read and write
|
||||
\033[32m7\033[0m = \033[35mrwx\033[0m = can read, write, execute
|
||||
|
||||
for directories/folders:
|
||||
\033[32m0\033[0m = \033[35m---\033[0m = no access
|
||||
\033[32m1\033[0m = \033[35m--x\033[0m = can read files in folder but not list contents
|
||||
\033[32m2\033[0m = \033[35m-w-\033[0m = n/a
|
||||
\033[32m3\033[0m = \033[35m-wx\033[0m = can create files but not list
|
||||
\033[32m4\033[0m = \033[35mr--\033[0m = can list, but not read/write
|
||||
\033[32m5\033[0m = \033[35mr-x\033[0m = can list and read files
|
||||
\033[32m6\033[0m = \033[35mrw-\033[0m = n/a
|
||||
\033[32m7\033[0m = \033[35mrwx\033[0m = can read, write, list
|
||||
"""
|
||||
),
|
||||
],
|
||||
[
|
||||
"pwhash",
|
||||
"password hashing",
|
||||
@@ -1013,10 +1051,13 @@ def add_upload(ap):
|
||||
ap2.add_argument("--reg-cap", metavar="N", type=int, default=38400, help="max number of uploads to keep in memory when running without \033[33m-e2d\033[0m; roughly 1 MiB RAM per 600")
|
||||
ap2.add_argument("--no-fpool", action="store_true", help="disable file-handle pooling -- instead, repeatedly close and reopen files during upload (bad idea to enable this on windows and/or cow filesystems)")
|
||||
ap2.add_argument("--use-fpool", action="store_true", help="force file-handle pooling, even when it might be dangerous (multiprocessing, filesystems lacking sparse-files support, ...)")
|
||||
ap2.add_argument("--chmod-f", metavar="UGO", type=u, default="", help="unix file permissions to use when creating files; default is probably 644 (OS-decided), see --help-chmod. Examples: [\033[32m644\033[0m] = owner-RW + all-R, [\033[32m755\033[0m] = owner-RWX + all-RX, [\033[32m777\033[0m] = full-yolo (volflag=chmod_f)")
|
||||
ap2.add_argument("--chmod-d", metavar="UGO", type=u, default="755", help="unix file permissions to use when creating directories; see --help-chmod. Examples: [\033[32m755\033[0m] = owner-RW + all-R, [\033[32m777\033[0m] = full-yolo (volflag=chmod_d)")
|
||||
ap2.add_argument("--dedup", action="store_true", help="enable symlink-based upload deduplication (volflag=dedup)")
|
||||
ap2.add_argument("--safe-dedup", metavar="N", type=int, default=50, help="how careful to be when deduplicating files; [\033[32m1\033[0m] = just verify the filesize, [\033[32m50\033[0m] = verify file contents have not been altered (volflag=safededup)")
|
||||
ap2.add_argument("--hardlink", action="store_true", help="enable hardlink-based dedup; will fallback on symlinks when that is impossible (across filesystems) (volflag=hardlink)")
|
||||
ap2.add_argument("--hardlink-only", action="store_true", help="do not fallback to symlinks when a hardlink cannot be made (volflag=hardlinkonly)")
|
||||
ap2.add_argument("--reflink", action="store_true", help="enable reflink-based dedup; will fallback on full copies when that is impossible (non-CoW filesystem) (volflag=reflink)")
|
||||
ap2.add_argument("--no-dupe", action="store_true", help="reject duplicate files during upload; only matches within the same volume (volflag=nodupe)")
|
||||
ap2.add_argument("--no-clone", action="store_true", help="do not use existing data on disk to satisfy dupe uploads; reduces server HDD reads in exchange for much more network load (volflag=noclone)")
|
||||
ap2.add_argument("--no-snap", action="store_true", help="disable snapshots -- forget unfinished uploads on shutdown; don't create .hist/up2k.snap files -- abandoned/interrupted uploads must be cleaned up manually")
|
||||
@@ -1049,7 +1090,7 @@ def add_network(ap):
|
||||
ap2.add_argument("--rp-loc", metavar="PATH", type=u, default="", help="if reverse-proxying on a location instead of a dedicated domain/subdomain, provide the base location here; example: [\033[32m/foo/bar\033[0m]")
|
||||
if ANYWIN:
|
||||
ap2.add_argument("--reuseaddr", action="store_true", help="set reuseaddr on listening sockets on windows; allows rapid restart of copyparty at the expense of being able to accidentally start multiple instances")
|
||||
else:
|
||||
elif not MACOS:
|
||||
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("--wr-h-eps", metavar="PATH", type=u, default="", help="write list of listening-on ip:port to textfile at \033[33mPATH\033[0m when http-servers have started")
|
||||
ap2.add_argument("--wr-h-aon", metavar="PATH", type=u, default="", help="write list of accessible-on ip:port to textfile at \033[33mPATH\033[0m when http-servers have started")
|
||||
@@ -1249,6 +1290,7 @@ def add_stats(ap):
|
||||
def add_yolo(ap):
|
||||
ap2 = ap.add_argument_group('yolo options')
|
||||
ap2.add_argument("--allow-csrf", action="store_true", help="disable csrf protections; let other domains/sites impersonate you through cross-site requests")
|
||||
ap2.add_argument("--cookie-lax", action="store_true", help="allow cookies from other domains (if you follow a link from another website into your server, you will arrive logged-in); this reduces protection against CSRF")
|
||||
ap2.add_argument("--getmod", action="store_true", help="permit ?move=[...] and ?delete as GET")
|
||||
ap2.add_argument("--wo-up-readme", action="store_true", help="allow users with write-only access to upload logues and readmes without adding the _wo_ filename prefix (volflag=wo_up_readme)")
|
||||
|
||||
@@ -1296,6 +1338,7 @@ def add_safety(ap):
|
||||
ap2.add_argument("--no-robots", action="store_true", help="adds http and html headers asking search engines to not index anything (volflag=norobots)")
|
||||
ap2.add_argument("--logout", metavar="H", type=float, default=8086.0, help="logout clients after \033[33mH\033[0m hours of inactivity; [\033[32m0.0028\033[0m]=10sec, [\033[32m0.1\033[0m]=6min, [\033[32m24\033[0m]=day, [\033[32m168\033[0m]=week, [\033[32m720\033[0m]=month, [\033[32m8760\033[0m]=year)")
|
||||
ap2.add_argument("--ban-pw", metavar="N,W,B", type=u, default="9,60,1440", help="more than \033[33mN\033[0m wrong passwords in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes; disable with [\033[32mno\033[0m]")
|
||||
ap2.add_argument("--ban-pwc", metavar="N,W,B", type=u, default="5,60,1440", help="more than \033[33mN\033[0m password-changes in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes; disable with [\033[32mno\033[0m]")
|
||||
ap2.add_argument("--ban-404", metavar="N,W,B", type=u, default="50,60,1440", help="hitting more than \033[33mN\033[0m 404's in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes; only affects users who cannot see directory listings because their access is either g/G/h")
|
||||
ap2.add_argument("--ban-403", metavar="N,W,B", type=u, default="9,2,1440", help="hitting more than \033[33mN\033[0m 403's in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes; [\033[32m1440\033[0m]=day, [\033[32m10080\033[0m]=week, [\033[32m43200\033[0m]=month")
|
||||
ap2.add_argument("--ban-422", metavar="N,W,B", type=u, default="9,2,1440", help="hitting more than \033[33mN\033[0m 422's in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes (invalid requests, attempted exploits ++)")
|
||||
@@ -1511,6 +1554,7 @@ def add_ui(ap, retry):
|
||||
ap2.add_argument("--nsort", action="store_true", help="default-enable natural sort of filenames with leading numbers (volflag=nsort)")
|
||||
ap2.add_argument("--hsortn", metavar="N", type=int, default=2, help="number of sorting rules to include in media URLs by default (volflag=hsortn)")
|
||||
ap2.add_argument("--see-dots", action="store_true", help="default-enable seeing dotfiles; only takes effect if user has the necessary permissions")
|
||||
ap2.add_argument("--qdel", metavar="LVL", type=int, default=2, help="number of confirmations to show when deleting files (2/1/0)")
|
||||
ap2.add_argument("--unlist", metavar="REGEX", type=u, default="", help="don't show files matching \033[33mREGEX\033[0m in file list. Purely cosmetic! Does not affect API calls, just the browser. Example: [\033[32m\\.(js|css)$\033[0m] (volflag=unlist)")
|
||||
ap2.add_argument("--favico", metavar="TXT", type=u, default="c 000 none" if retry else "🎉 000 none", help="\033[33mfavicon-text\033[0m [ \033[33mforeground\033[0m [ \033[33mbackground\033[0m ] ], set blank to disable")
|
||||
ap2.add_argument("--ext-th", metavar="E=VP", type=u, action="append", help="use thumbnail-image \033[33mVP\033[0m for file-extension \033[33mE\033[0m, example: [\033[32mexe=/.res/exe.png\033[0m] (volflag=ext_th)")
|
||||
@@ -1525,7 +1569,7 @@ def add_ui(ap, retry):
|
||||
ap2.add_argument("--txt-max", metavar="KiB", type=int, default=64, help="max size of embedded textfiles on ?doc= (anything bigger will be lazy-loaded by JS)")
|
||||
ap2.add_argument("--doctitle", metavar="TXT", type=u, default="copyparty @ --name", help="title / service-name to show in html documents")
|
||||
ap2.add_argument("--bname", metavar="TXT", type=u, default="--name", help="server name (displayed in filebrowser document title)")
|
||||
ap2.add_argument("--pb-url", metavar="URL", type=u, default=URL_PRJ, help="powered-by link; disable with \033[33m-np\033[0m")
|
||||
ap2.add_argument("--pb-url", metavar="URL", type=u, default=URL_PRJ, help="powered-by link; disable with \033[33m-nb\033[0m")
|
||||
ap2.add_argument("--ver", action="store_true", help="show version on the control panel (incompatible with \033[33m-nb\033[0m)")
|
||||
ap2.add_argument("--k304", metavar="NUM", type=int, default=0, help="configure the option to enable/disable k304 on the controlpanel (workaround for buggy reverse-proxies); [\033[32m0\033[0m] = hidden and default-off, [\033[32m1\033[0m] = visible and default-off, [\033[32m2\033[0m] = visible and default-on")
|
||||
ap2.add_argument("--no304", metavar="NUM", type=int, default=0, help="configure the option to enable/disable no304 on the controlpanel (workaround for buggy caching in browsers); [\033[32m0\033[0m] = hidden and default-off, [\033[32m1\033[0m] = visible and default-off, [\033[32m2\033[0m] = visible and default-on")
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# coding: utf-8
|
||||
|
||||
VERSION = (1, 18, 2)
|
||||
VERSION = (1, 18, 6)
|
||||
CODENAME = "logtail"
|
||||
BUILD_DT = (2025, 7, 7)
|
||||
BUILD_DT = (2025, 7, 28)
|
||||
|
||||
S_VERSION = ".".join(map(str, VERSION))
|
||||
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)
|
||||
|
||||
@@ -120,6 +120,8 @@ class Lim(object):
|
||||
|
||||
self.reg: Optional[dict[str, dict[str, Any]]] = None # up2k registry
|
||||
|
||||
self.chmod_d = 0o755
|
||||
|
||||
self.nups: dict[str, list[float]] = {} # num tracker
|
||||
self.bups: dict[str, list[tuple[float, int]]] = {} # byte tracker list
|
||||
self.bupc: dict[str, int] = {} # byte tracker cache
|
||||
@@ -280,7 +282,7 @@ class Lim(object):
|
||||
if not dirs:
|
||||
# no branches yet; make one
|
||||
sub = os.path.join(path, "0")
|
||||
bos.mkdir(sub)
|
||||
bos.mkdir(sub, self.chmod_d)
|
||||
else:
|
||||
# try newest branch only
|
||||
sub = os.path.join(path, str(dirs[-1]))
|
||||
@@ -295,7 +297,7 @@ class Lim(object):
|
||||
|
||||
# make a branch
|
||||
sub = os.path.join(path, str(dirs[-1] + 1))
|
||||
bos.mkdir(sub)
|
||||
bos.mkdir(sub, self.chmod_d)
|
||||
ret = self.dive(sub, lvs - 1)
|
||||
if ret is None:
|
||||
raise Pebkac(500, "rotation bug")
|
||||
@@ -372,6 +374,7 @@ class VFS(object):
|
||||
self.shr_src: Optional[tuple[VFS, str]] = None # source vfs+rem of a share
|
||||
self.shr_files: set[str] = set() # filenames to include from shr_src
|
||||
self.shr_owner: str = "" # uname
|
||||
self.shr_all_aps: list[tuple[str, list[VFS]]] = []
|
||||
self.aread: dict[str, list[str]] = {}
|
||||
self.awrite: dict[str, list[str]] = {}
|
||||
self.amove: dict[str, list[str]] = {}
|
||||
@@ -383,20 +386,20 @@ class VFS(object):
|
||||
self.adot: dict[str, list[str]] = {}
|
||||
self.js_ls = {}
|
||||
self.js_htm = ""
|
||||
self.all_vols: dict[str, VFS] = {} # flattened recursive
|
||||
self.all_nodes: dict[str, VFS] = {} # also jumpvols/shares
|
||||
|
||||
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.dbpath = self.histpath
|
||||
self.all_vols = {vpath: self} # flattened recursive
|
||||
self.all_nodes = {vpath: self} # also jumpvols/shares
|
||||
self.all_aps = [(rp, self)]
|
||||
self.all_vols[vpath] = self
|
||||
self.all_nodes[vpath] = self
|
||||
self.all_aps = [(rp, [self])]
|
||||
self.all_vps = [(vp, self)]
|
||||
else:
|
||||
self.histpath = self.dbpath = ""
|
||||
self.all_vols = {}
|
||||
self.all_nodes = {}
|
||||
self.all_aps = []
|
||||
self.all_vps = []
|
||||
|
||||
@@ -415,7 +418,7 @@ class VFS(object):
|
||||
self,
|
||||
vols: dict[str, "VFS"],
|
||||
nodes: dict[str, "VFS"],
|
||||
aps: list[tuple[str, "VFS"]],
|
||||
aps: list[tuple[str, list["VFS"]]],
|
||||
vps: list[tuple[str, "VFS"]],
|
||||
) -> None:
|
||||
nodes[self.vpath] = self
|
||||
@@ -424,7 +427,11 @@ class VFS(object):
|
||||
rp = self.realpath
|
||||
rp += "" if rp.endswith(os.sep) else os.sep
|
||||
vp = self.vpath + ("/" if self.vpath else "")
|
||||
aps.append((rp, self))
|
||||
hit = next((x[1] for x in aps if x[0] == rp), None)
|
||||
if hit:
|
||||
hit.append(self)
|
||||
else:
|
||||
aps.append((rp, [self]))
|
||||
vps.append((vp, self))
|
||||
|
||||
for v in self.nodes.values():
|
||||
@@ -848,9 +855,11 @@ class VFS(object):
|
||||
return None
|
||||
|
||||
if "xvol" in self.flags:
|
||||
for vap, vn in self.root.all_aps:
|
||||
all_aps = self.shr_all_aps or self.root.all_aps
|
||||
|
||||
for vap, vns in all_aps:
|
||||
if aps.startswith(vap):
|
||||
return vn
|
||||
return self if self in vns else vns[0]
|
||||
|
||||
if self.log:
|
||||
self.log("vfs", "xvol: %r" % (ap,), 3)
|
||||
@@ -859,6 +868,53 @@ class VFS(object):
|
||||
|
||||
return self
|
||||
|
||||
def check_landmarks(self) -> bool:
|
||||
if self.dbv:
|
||||
return True
|
||||
|
||||
vps = self.flags.get("landmark") or []
|
||||
if not vps:
|
||||
return True
|
||||
|
||||
failed = ""
|
||||
for vp in vps:
|
||||
if "^=" in vp:
|
||||
vp, zs = vp.split("^=", 1)
|
||||
expect = zs.encode("utf-8")
|
||||
else:
|
||||
expect = b""
|
||||
|
||||
if self.log:
|
||||
t = "checking [/%s] landmark [%s]"
|
||||
self.log("vfs", t % (self.vpath, vp), 6)
|
||||
|
||||
ap = "?"
|
||||
try:
|
||||
ap = self.canonical(vp)
|
||||
with open(ap, "rb") as f:
|
||||
buf = f.read(4096)
|
||||
if not buf.startswith(expect):
|
||||
t = "file [%s] does not start with the expected bytes %s"
|
||||
failed = t % (ap, expect)
|
||||
break
|
||||
except Exception as ex:
|
||||
t = "%r while trying to read [%s] => [%s]"
|
||||
failed = t % (ex, vp, ap)
|
||||
break
|
||||
|
||||
if not failed:
|
||||
return True
|
||||
|
||||
if self.log:
|
||||
t = "WARNING: landmark verification failed; %s; will now disable up2k database for volume [/%s]"
|
||||
self.log("vfs", t % (failed, self.vpath), 3)
|
||||
|
||||
for rm in "e2d e2t e2v".split():
|
||||
self.flags = {k: v for k, v in self.flags.items() if not k.startswith(rm)}
|
||||
self.flags["d2d"] = True
|
||||
self.flags["d2t"] = True
|
||||
return False
|
||||
|
||||
|
||||
if WINDOWS:
|
||||
re_vol = re.compile(r"^([a-zA-Z]:[\\/][^:]*|[^:]*):([^:]*):(.*)$")
|
||||
@@ -922,6 +978,9 @@ class AuthSrv(object):
|
||||
|
||||
yield prev, True
|
||||
|
||||
def vf0(self):
|
||||
return {"d2d": True, "tcolor": self.args.tcolor}
|
||||
|
||||
def idp_checkin(
|
||||
self, broker: Optional["BrokerCli"], uname: str, gname: str
|
||||
) -> bool:
|
||||
@@ -1489,7 +1548,7 @@ class AuthSrv(object):
|
||||
flags[name] = True
|
||||
return
|
||||
|
||||
zs = "ext_th mtp on403 on404 xbu xau xiu xbc xac xbr xar xbd xad xm xban"
|
||||
zs = "ext_th landmark mtp on403 on404 xbu xau xiu xbc xac xbr xar xbd xad xm xban"
|
||||
if name not in zs.split():
|
||||
if value is True:
|
||||
t = "└─add volflag [{}] = {} ({})"
|
||||
@@ -1628,13 +1687,12 @@ class AuthSrv(object):
|
||||
t = "Read-access has been disabled due to failsafe: No volumes were defined by the config-file. This failsafe is to prevent unintended access if this is due to accidental loss of config. You can override this safeguard and allow read/write to the working-directory by adding the following arguments: -v .::rw"
|
||||
self.log(t, 1)
|
||||
axs = AXS()
|
||||
vfs = VFS(self.log_func, absreal("."), "", "", axs, {})
|
||||
vfs = VFS(self.log_func, absreal("."), "", "", axs, self.vf0())
|
||||
if not axs.uread:
|
||||
self.badcfg1 = True
|
||||
elif "" not in mount:
|
||||
# there's volumes but no root; make root inaccessible
|
||||
zsd = {"d2d": True, "tcolor": self.args.tcolor}
|
||||
vfs = VFS(self.log_func, "", "", "", AXS(), zsd)
|
||||
vfs = VFS(self.log_func, "", "", "", AXS(), self.vf0())
|
||||
|
||||
maxdepth = 0
|
||||
for dst in sorted(mount.keys(), key=lambda x: (x.count("/"), len(x))):
|
||||
@@ -1683,8 +1741,7 @@ class AuthSrv(object):
|
||||
if enshare:
|
||||
assert sqlite3 # type: ignore # !rm
|
||||
|
||||
zsd = {"d2d": True, "tcolor": self.args.tcolor}
|
||||
shv = VFS(self.log_func, "", shr, shr, AXS(), zsd)
|
||||
shv = VFS(self.log_func, "", shr, shr, AXS(), self.vf0())
|
||||
|
||||
db_path = self.args.shr_db
|
||||
db = sqlite3.connect(db_path)
|
||||
@@ -2066,6 +2123,8 @@ class AuthSrv(object):
|
||||
|
||||
all_mte = {}
|
||||
errors = False
|
||||
free_umask = False
|
||||
have_reflink = False
|
||||
for vol in vfs.all_nodes.values():
|
||||
if (self.args.e2ds and vol.axs.uwrite) or self.args.e2dsa:
|
||||
vol.flags["e2ds"] = True
|
||||
@@ -2122,12 +2181,36 @@ class AuthSrv(object):
|
||||
t = 'volume "/%s" has invalid %stry [%s]'
|
||||
raise Exception(t % (vol.vpath, k, vol.flags.get(k + "try")))
|
||||
|
||||
for k in ("chmod_d", "chmod_f"):
|
||||
is_d = k == "chmod_d"
|
||||
zs = vol.flags.get(k, "")
|
||||
if not zs and is_d:
|
||||
zs = "755"
|
||||
if not zs:
|
||||
vol.flags.pop(k, None)
|
||||
continue
|
||||
if not re.match("^[0-7]{3}$", zs):
|
||||
t = "config-option '%s' must be a three-digit octal value such as [755] or [644] but the value was [%s]"
|
||||
t = t % (k, zs)
|
||||
self.log(t, 1)
|
||||
raise Exception(t)
|
||||
zi = int(zs, 8)
|
||||
vol.flags[k] = zi
|
||||
if (is_d and zi != 0o755) or not is_d:
|
||||
free_umask = True
|
||||
|
||||
if vol.lim:
|
||||
vol.lim.chmod_d = vol.flags["chmod_d"]
|
||||
|
||||
if vol.flags.get("og"):
|
||||
self.args.uqe = True
|
||||
|
||||
if "unlistcr" in vol.flags or "unlistcw" in vol.flags:
|
||||
self.args.have_unlistc = True
|
||||
|
||||
if "reflink" in vol.flags:
|
||||
have_reflink = True
|
||||
|
||||
zs = str(vol.flags.get("tcolor", "")).lstrip("#")
|
||||
if len(zs) == 3: # fc5 => ffcc55
|
||||
vol.flags["tcolor"] = "".join([x * 2 for x in zs])
|
||||
@@ -2205,6 +2288,8 @@ class AuthSrv(object):
|
||||
t = "WARNING: volume [/%s]: invalid value specified for ext-th: %s"
|
||||
self.log(t % (vol.vpath, etv), 3)
|
||||
|
||||
vol.check_landmarks()
|
||||
|
||||
# d2d drops all database features for a volume
|
||||
for grp, rm in [["d2d", "e2d"], ["d2t", "e2t"], ["d2d", "e2v"]]:
|
||||
if not vol.flags.get(grp, False):
|
||||
@@ -2351,6 +2436,10 @@ class AuthSrv(object):
|
||||
if errors:
|
||||
sys.exit(1)
|
||||
|
||||
setattr(self.args, "free_umask", free_umask)
|
||||
if free_umask:
|
||||
os.umask(0)
|
||||
|
||||
vfs.bubble_flags()
|
||||
|
||||
have_e2d = False
|
||||
@@ -2486,6 +2575,13 @@ class AuthSrv(object):
|
||||
t = "WARNING! The following IdP volumes are mounted below another volume where other users can read and/or write files. This is a SECURITY HAZARD!! When copyparty is restarted, it will not know about these IdP volumes yet. These volumes will then be accessible by an unexpected set of permissions UNTIL one of the users associated with their volume sends a request to the server. RECOMMENDATION: You should create a restricted volume where nobody can read/write files, and make sure that all IdP volumes are configured to appear somewhere below that volume."
|
||||
self.log(t + "".join(self.idp_err), 1)
|
||||
|
||||
if have_reflink:
|
||||
t = "WARNING: Reflink-based dedup was requested, but %s. This will not work; files will be full copies instead."
|
||||
if sys.version_info < (3, 14):
|
||||
self.log(t % "your python version is not new enough", 1)
|
||||
if not sys.platform.startswith("linux"):
|
||||
self.log(t % "your OS is not Linux", 1)
|
||||
|
||||
self.vfs = vfs
|
||||
self.acct = acct
|
||||
self.defpw = defpw
|
||||
@@ -2553,6 +2649,28 @@ class AuthSrv(object):
|
||||
shn.shr_src = (s_vfs, s_rem)
|
||||
shn.realpath = s_vfs.canonical(s_rem)
|
||||
|
||||
# root.all_aps doesn't include any shares, so make a copy where the
|
||||
# share appears in all abspaths it can provide (for example for chk_ap)
|
||||
ap = shn.realpath
|
||||
if not ap.endswith(os.sep):
|
||||
ap += os.sep
|
||||
shn.shr_all_aps = [(x, y[:]) for x, y in vfs.all_aps]
|
||||
exact = False
|
||||
for ap2, vns in shn.shr_all_aps:
|
||||
if ap == ap2:
|
||||
exact = True
|
||||
if ap2.startswith(ap):
|
||||
try:
|
||||
vp2 = vjoin(s_rem, ap2[len(ap) :])
|
||||
vn2, _ = s_vfs.get(vp2, "*", False, False)
|
||||
if vn2 == s_vfs or vn2.dbv == s_vfs:
|
||||
vns.append(shn)
|
||||
except:
|
||||
pass
|
||||
if not exact:
|
||||
shn.shr_all_aps.append((ap, [shn]))
|
||||
shn.shr_all_aps.sort(key=lambda x: len(x[0]), reverse=True)
|
||||
|
||||
if self.args.shr_v:
|
||||
t = "mapped %s share [%s] by [%s] => [%s] => [%s]"
|
||||
self.log(t % (s_pr, s_k, s_un, s_vp, shn.realpath))
|
||||
@@ -2567,7 +2685,7 @@ class AuthSrv(object):
|
||||
continue # also fine
|
||||
for zs in svn.nodes.keys():
|
||||
# hide subvolume
|
||||
vn.nodes[zs] = VFS(self.log_func, "", "", "", AXS(), {})
|
||||
vn.nodes[zs] = VFS(self.log_func, "", "", "", AXS(), self.vf0())
|
||||
|
||||
cur2.close()
|
||||
cur.close()
|
||||
@@ -2612,6 +2730,7 @@ class AuthSrv(object):
|
||||
"def_hcols": list(vf.get("mth") or []),
|
||||
"unlist0": vf.get("unlist") or "",
|
||||
"see_dots": self.args.see_dots,
|
||||
"dqdel": self.args.qdel,
|
||||
"dgrid": "grid" in vf,
|
||||
"dgsel": "gsel" in vf,
|
||||
"dnsort": "nsort" in vf,
|
||||
|
||||
@@ -25,14 +25,26 @@ def listdir(p: str = ".") -> list[str]:
|
||||
|
||||
|
||||
def makedirs(name: str, mode: int = 0o755, exist_ok: bool = True) -> bool:
|
||||
# os.makedirs does 777 for all but leaf; this does mode on all
|
||||
todo = []
|
||||
bname = fsenc(name)
|
||||
try:
|
||||
os.makedirs(bname, mode)
|
||||
return True
|
||||
except:
|
||||
if not exist_ok or not os.path.isdir(bname):
|
||||
raise
|
||||
while bname:
|
||||
if os.path.isdir(bname):
|
||||
break
|
||||
todo.append(bname)
|
||||
bname = os.path.dirname(bname)
|
||||
if not todo:
|
||||
if not exist_ok:
|
||||
os.mkdir(bname) # to throw
|
||||
return False
|
||||
for zb in todo[::-1]:
|
||||
try:
|
||||
os.mkdir(zb, mode)
|
||||
except:
|
||||
if os.path.isdir(zb):
|
||||
continue
|
||||
raise
|
||||
return True
|
||||
|
||||
|
||||
def mkdir(p: str, mode: int = 0o755) -> None:
|
||||
|
||||
@@ -52,6 +52,7 @@ def vf_bmap() -> dict[str, str]:
|
||||
"og_no_head",
|
||||
"og_s_title",
|
||||
"rand",
|
||||
"reflink",
|
||||
"rmagic",
|
||||
"rss",
|
||||
"wo_up_readme",
|
||||
@@ -78,6 +79,8 @@ def vf_vmap() -> dict[str, str]:
|
||||
}
|
||||
for k in (
|
||||
"bup_ck",
|
||||
"chmod_d",
|
||||
"chmod_f",
|
||||
"dbd",
|
||||
"forget_ip",
|
||||
"hsortn",
|
||||
@@ -166,9 +169,12 @@ flagcats = {
|
||||
"dedup": "enable symlink-based file deduplication",
|
||||
"hardlink": "enable hardlink-based file deduplication,\nwith fallback on symlinks when that is impossible",
|
||||
"hardlinkonly": "dedup with hardlink only, never symlink;\nmake a full copy if hardlink is impossible",
|
||||
"reflink": "enable reflink-based file deduplication,\nwith fallback on full copy when that is impossible",
|
||||
"safededup": "verify on-disk data before using it for dedup",
|
||||
"noclone": "take dupe data from clients, even if available on HDD",
|
||||
"nodupe": "rejects existing files (instead of linking/cloning them)",
|
||||
"chmod_d=755": "unix-permission for new dirs/folders",
|
||||
"chmod_f=644": "unix-permission for new files",
|
||||
"sparse": "force use of sparse files, mainly for s3-backed storage",
|
||||
"nosparse": "deny use of sparse files, mainly for slow storage",
|
||||
"daw": "enable full WebDAV write support (dangerous);\nPUT-operations will now \033[1;31mOVERWRITE\033[0;35m existing files",
|
||||
@@ -218,6 +224,7 @@ flagcats = {
|
||||
"d2d": "disables all database stuff, overrides -e2*",
|
||||
"hist=/tmp/cdb": "puts thumbnails and indexes at that location",
|
||||
"dbpath=/tmp/cdb": "puts indexes at that location",
|
||||
"landmark=foo": "disable db if file foo doesn't exist",
|
||||
"scan=60": "scan for new files every 60sec, same as --re-maxage",
|
||||
"nohash=\\.iso$": "skips hashing file contents if path matches *.iso",
|
||||
"noidx=\\.iso$": "fully ignores the contents at paths matching *.iso",
|
||||
|
||||
@@ -229,7 +229,7 @@ class FtpFs(AbstractedFS):
|
||||
r = "r" in mode
|
||||
w = "w" in mode or "a" in mode or "+" in mode
|
||||
|
||||
ap = self.rv2a(filename, r, w)[0]
|
||||
ap, vfs, _ = self.rv2a(filename, r, w)
|
||||
self.validpath(ap)
|
||||
if w:
|
||||
try:
|
||||
@@ -261,7 +261,11 @@ class FtpFs(AbstractedFS):
|
||||
|
||||
wunlink(self.log, ap, VF_CAREFUL)
|
||||
|
||||
return open(fsenc(ap), mode, self.args.iobuf)
|
||||
ret = open(fsenc(ap), mode, self.args.iobuf)
|
||||
if w and "chmod_f" in vfs.flags:
|
||||
os.fchmod(ret.fileno(), vfs.flags["chmod_f"])
|
||||
|
||||
return ret
|
||||
|
||||
def chdir(self, path: str) -> None:
|
||||
nwd = join(self.cwd, path)
|
||||
@@ -292,8 +296,9 @@ class FtpFs(AbstractedFS):
|
||||
) = 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
|
||||
ap, vfs, _ = self.rv2a(path, w=True)
|
||||
chmod = vfs.flags["chmod_d"]
|
||||
bos.makedirs(ap, chmod) # filezilla expects this
|
||||
|
||||
def listdir(self, path: str) -> list[str]:
|
||||
vpath = join(self.cwd, path)
|
||||
|
||||
@@ -45,6 +45,7 @@ from .util import (
|
||||
APPLESAN_RE,
|
||||
BITNESS,
|
||||
DAV_ALLPROPS,
|
||||
E_SCK_WR,
|
||||
FN_EMB,
|
||||
HAVE_SQLITE3,
|
||||
HTTPCODE,
|
||||
@@ -1374,12 +1375,13 @@ class HttpCli(object):
|
||||
title = self.uparam.get("title") or self.vpath.split("/")[-1]
|
||||
etitle = html_escape(title, True, True)
|
||||
|
||||
baseurl = "%s://%s%s" % (
|
||||
baseurl = "%s://%s/" % (
|
||||
"https" if self.is_https else "http",
|
||||
self.host,
|
||||
self.args.SRS,
|
||||
)
|
||||
feed = "%s%s" % (baseurl, self.req[1:])
|
||||
feed = baseurl + self.req[1:]
|
||||
if self.is_vproxied:
|
||||
baseurl += self.args.RS
|
||||
efeed = html_escape(feed, True, True)
|
||||
edirlink = efeed.split("?")[0] + q_pw
|
||||
|
||||
@@ -1392,7 +1394,7 @@ class HttpCli(object):
|
||||
\t\t<title>%s</title>
|
||||
\t\t<description></description>
|
||||
\t\t<link>%s</link>
|
||||
\t\t<generator>copyparty-1</generator>
|
||||
\t\t<generator>copyparty-2</generator>
|
||||
"""
|
||||
% (efeed, etitle, edirlink)
|
||||
]
|
||||
@@ -2066,7 +2068,7 @@ class HttpCli(object):
|
||||
fdir, fn = os.path.split(fdir)
|
||||
rem, _ = vsplit(rem)
|
||||
|
||||
bos.makedirs(fdir)
|
||||
bos.makedirs(fdir, vfs.flags["chmod_d"])
|
||||
|
||||
open_ka: dict[str, Any] = {"fun": open}
|
||||
open_a = ["wb", self.args.iobuf]
|
||||
@@ -2125,6 +2127,8 @@ class HttpCli(object):
|
||||
fn = vfs.flags["put_name2"].format(now=time.time(), cip=self.dip())
|
||||
|
||||
params = {"suffix": suffix, "fdir": fdir}
|
||||
if "chmod_f" in vfs.flags:
|
||||
params["chmod"] = vfs.flags["chmod_f"]
|
||||
if self.args.nw:
|
||||
params = {}
|
||||
fn = os.devnull
|
||||
@@ -2173,7 +2177,7 @@ class HttpCli(object):
|
||||
if self.args.nw:
|
||||
fn = os.devnull
|
||||
else:
|
||||
bos.makedirs(fdir)
|
||||
bos.makedirs(fdir, vfs.flags["chmod_d"])
|
||||
path = os.path.join(fdir, fn)
|
||||
if not nameless:
|
||||
self.vpath = vjoin(self.vpath, fn)
|
||||
@@ -2305,7 +2309,7 @@ class HttpCli(object):
|
||||
if self.args.hook_v:
|
||||
log_reloc(self.log, hr["reloc"], x, path, vp, fn, vfs, rem)
|
||||
fdir, self.vpath, fn, (vfs, rem) = x
|
||||
bos.makedirs(fdir)
|
||||
bos.makedirs(fdir, vfs.flags["chmod_d"])
|
||||
path2 = os.path.join(fdir, fn)
|
||||
atomic_move(self.log, path, path2, vfs.flags)
|
||||
path = path2
|
||||
@@ -2591,7 +2595,7 @@ class HttpCli(object):
|
||||
dst = vfs.canonical(rem)
|
||||
try:
|
||||
if not bos.path.isdir(dst):
|
||||
bos.makedirs(dst)
|
||||
bos.makedirs(dst, vfs.flags["chmod_d"])
|
||||
except OSError as ex:
|
||||
self.log("makedirs failed %r" % (dst,))
|
||||
if not bos.path.isdir(dst):
|
||||
@@ -2908,6 +2912,7 @@ class HttpCli(object):
|
||||
|
||||
ok, msg = self.asrv.chpw(self.conn.hsrv.broker, self.uname, pwd)
|
||||
if ok:
|
||||
self.cbonk(self.conn.hsrv.gpwc, pwd, "pw", "too many password changes")
|
||||
ok, msg = self.get_pwd_cookie(pwd)
|
||||
if ok:
|
||||
msg = "new password OK"
|
||||
@@ -2991,12 +2996,20 @@ class HttpCli(object):
|
||||
# reset both plaintext and tls
|
||||
# (only affects active tls cookies when tls)
|
||||
for k in ("cppwd", "cppws") if self.is_https else ("cppwd",):
|
||||
ck = gencookie(k, pwd, self.args.R, False)
|
||||
ck = gencookie(k, pwd, self.args.R, self.args.cookie_lax, False)
|
||||
self.out_headerlist.append(("Set-Cookie", ck))
|
||||
self.out_headers.pop("Set-Cookie", None) # drop keepalive
|
||||
else:
|
||||
k = "cppws" if self.is_https else "cppwd"
|
||||
ck = gencookie(k, pwd, self.args.R, self.is_https, dur, "; HttpOnly")
|
||||
ck = gencookie(
|
||||
k,
|
||||
pwd,
|
||||
self.args.R,
|
||||
self.args.cookie_lax,
|
||||
self.is_https,
|
||||
dur,
|
||||
"; HttpOnly",
|
||||
)
|
||||
self.out_headers["Set-Cookie"] = ck
|
||||
|
||||
return dur > 0, msg
|
||||
@@ -3013,6 +3026,9 @@ class HttpCli(object):
|
||||
self.gctx = vpath
|
||||
vpath = undot(vpath)
|
||||
vfs, rem = self.asrv.vfs.get(vpath, self.uname, False, True)
|
||||
if "nosub" in vfs.flags:
|
||||
raise Pebkac(403, "mkdir is forbidden below this folder")
|
||||
|
||||
rem = sanitize_vpath(rem, "/")
|
||||
fn = vfs.canonical(rem)
|
||||
|
||||
@@ -3026,7 +3042,7 @@ class HttpCli(object):
|
||||
raise Pebkac(405, 'folder "/%s" already exists' % (vpath,))
|
||||
|
||||
try:
|
||||
bos.makedirs(fn)
|
||||
bos.makedirs(fn, vfs.flags["chmod_d"])
|
||||
except OSError as ex:
|
||||
if ex.errno == errno.EACCES:
|
||||
raise Pebkac(500, "the server OS denied write-access")
|
||||
@@ -3068,6 +3084,8 @@ class HttpCli(object):
|
||||
|
||||
with open(fsenc(fn), "wb") as f:
|
||||
f.write(b"`GRUNNUR`\n")
|
||||
if "chmod_f" in vfs.flags:
|
||||
os.fchmod(f.fileno(), vfs.flags["chmod_f"])
|
||||
|
||||
vpath = "{}/{}".format(self.vpath, sanitized).lstrip("/")
|
||||
self.redirect(vpath, "?edit")
|
||||
@@ -3141,7 +3159,7 @@ class HttpCli(object):
|
||||
)
|
||||
upload_vpath = "{}/{}".format(vfs.vpath, rem).strip("/")
|
||||
if not nullwrite:
|
||||
bos.makedirs(fdir_base)
|
||||
bos.makedirs(fdir_base, vfs.flags["chmod_d"])
|
||||
|
||||
rnd, lifetime, xbu, xau = self.upload_flags(vfs)
|
||||
zs = self.uparam.get("want") or self.headers.get("accept") or ""
|
||||
@@ -3236,8 +3254,11 @@ class HttpCli(object):
|
||||
else:
|
||||
open_args["fdir"] = fdir
|
||||
|
||||
if "chmod_f" in vfs.flags:
|
||||
open_args["chmod"] = vfs.flags["chmod_f"]
|
||||
|
||||
if p_file and not nullwrite:
|
||||
bos.makedirs(fdir)
|
||||
bos.makedirs(fdir, vfs.flags["chmod_d"])
|
||||
|
||||
# reserve destination filename
|
||||
f, fname = ren_open(fname, "wb", fdir=fdir, suffix=suffix)
|
||||
@@ -3341,7 +3362,7 @@ class HttpCli(object):
|
||||
if nullwrite:
|
||||
fdir = ap2 = ""
|
||||
else:
|
||||
bos.makedirs(fdir)
|
||||
bos.makedirs(fdir, vfs.flags["chmod_d"])
|
||||
atomic_move(self.log, abspath, ap2, vfs.flags)
|
||||
abspath = ap2
|
||||
sz = bos.path.getsize(abspath)
|
||||
@@ -3462,6 +3483,8 @@ class HttpCli(object):
|
||||
ft = "{}:{}".format(self.ip, self.addr[1])
|
||||
ft = "{}\n{}\n{}\n".format(ft, msg.rstrip(), errmsg)
|
||||
f.write(ft.encode("utf-8"))
|
||||
if "chmod_f" in vfs.flags:
|
||||
os.fchmod(f.fileno(), vfs.flags["chmod_f"])
|
||||
except Exception as ex:
|
||||
suf = "\nfailed to write the upload report: {}".format(ex)
|
||||
|
||||
@@ -3512,7 +3535,7 @@ class HttpCli(object):
|
||||
lim = vfs.get_dbv(rem)[0].lim
|
||||
if lim:
|
||||
fp, rp = lim.all(self.ip, rp, clen, vfs.realpath, fp, self.conn.hsrv.broker)
|
||||
bos.makedirs(fp)
|
||||
bos.makedirs(fp, vfs.flags["chmod_d"])
|
||||
|
||||
fp = os.path.join(fp, fn)
|
||||
rem = "{}/{}".format(rp, fn).strip("/")
|
||||
@@ -3580,13 +3603,15 @@ class HttpCli(object):
|
||||
zs = ub64enc(zb).decode("ascii")[:24].lower()
|
||||
dp = "%s/md/%s/%s/%s" % (dbv.histpath, zs[:2], zs[2:4], zs)
|
||||
self.log("moving old version to %s/%s" % (dp, mfile2))
|
||||
if bos.makedirs(dp):
|
||||
if bos.makedirs(dp, vfs.flags["chmod_d"]):
|
||||
with open(os.path.join(dp, "dir.txt"), "wb") as f:
|
||||
f.write(afsenc(vrd))
|
||||
if "chmod_f" in vfs.flags:
|
||||
os.fchmod(f.fileno(), vfs.flags["chmod_f"])
|
||||
elif hist_cfg == "s":
|
||||
dp = os.path.join(mdir, ".hist")
|
||||
try:
|
||||
bos.mkdir(dp)
|
||||
bos.mkdir(dp, vfs.flags["chmod_d"])
|
||||
hidedir(dp)
|
||||
except:
|
||||
pass
|
||||
@@ -3625,6 +3650,8 @@ class HttpCli(object):
|
||||
wunlink(self.log, fp, vfs.flags)
|
||||
|
||||
with open(fsenc(fp), "wb", self.args.iobuf) as f:
|
||||
if "chmod_f" in vfs.flags:
|
||||
os.fchmod(f.fileno(), vfs.flags["chmod_f"])
|
||||
sz, sha512, _ = hashcopy(p_data, f, None, 0, self.args.s_wr_slp)
|
||||
|
||||
if lim:
|
||||
@@ -4364,7 +4391,7 @@ class HttpCli(object):
|
||||
self.log("file deleted; disconnecting")
|
||||
break
|
||||
except IOError as ex:
|
||||
if ex.errno not in (errno.EPIPE, errno.ESHUTDOWN, errno.EBADFD):
|
||||
if ex.errno not in E_SCK_WR:
|
||||
raise
|
||||
finally:
|
||||
if f:
|
||||
@@ -4850,13 +4877,21 @@ class HttpCli(object):
|
||||
def tx_svcs(self) -> bool:
|
||||
aname = re.sub("[^0-9a-zA-Z]+", "", self.args.vname) or "a"
|
||||
ep = self.host
|
||||
host = ep.split(":")[0]
|
||||
hport = ep[ep.find(":") :] if ":" in ep else ""
|
||||
rip = (
|
||||
host
|
||||
if self.args.rclone_mdns or not self.args.zm
|
||||
else self.conn.hsrv.nm.map(self.ip) or host
|
||||
)
|
||||
sep = "]:" if "]" in ep else ":"
|
||||
if sep in ep:
|
||||
host, hport = ep.rsplit(":", 1)
|
||||
hport = ":" + hport
|
||||
else:
|
||||
host = ep
|
||||
hport = ""
|
||||
|
||||
if host.endswith(".local") and self.args.zm and not self.args.rclone_mdns:
|
||||
rip = self.conn.hsrv.nm.map(self.ip) or host
|
||||
if ":" in rip and "[" not in rip:
|
||||
rip = "[%s]" % (rip,)
|
||||
else:
|
||||
rip = host
|
||||
|
||||
# safer than html_escape/quotep since this avoids both XSS and shell-stuff
|
||||
pw = re.sub(r"[<>&$?`\"']", "_", self.pw or "hunter2")
|
||||
vp = re.sub(r"[<>&$?`\"']", "_", self.uparam["hc"] or "").lstrip("/")
|
||||
@@ -5026,7 +5061,7 @@ class HttpCli(object):
|
||||
def setck(self) -> bool:
|
||||
k, v = self.uparam["setck"].split("=", 1)
|
||||
t = 0 if v in ("", "x") else 86400 * 299
|
||||
ck = gencookie(k, v, self.args.R, False, t)
|
||||
ck = gencookie(k, v, self.args.R, self.args.cookie_lax, False, t)
|
||||
self.out_headerlist.append(("Set-Cookie", ck))
|
||||
if "cc" in self.ouparam:
|
||||
self.redirect("", "?h#cc")
|
||||
@@ -5038,7 +5073,7 @@ class HttpCli(object):
|
||||
for k in ALL_COOKIES:
|
||||
if k not in self.cookies:
|
||||
continue
|
||||
cookie = gencookie(k, "x", self.args.R, False)
|
||||
cookie = gencookie(k, "x", self.args.R, self.args.cookie_lax, False)
|
||||
self.out_headerlist.append(("Set-Cookie", cookie))
|
||||
|
||||
self.redirect("", "?h#cc")
|
||||
@@ -5558,7 +5593,7 @@ class HttpCli(object):
|
||||
db.commit()
|
||||
db.close()
|
||||
|
||||
self.conn.hsrv.broker.ask("reload", False, False).get()
|
||||
self.conn.hsrv.broker.ask("reload", False, True).get()
|
||||
|
||||
self.redirect("", "?idp")
|
||||
return True
|
||||
@@ -5642,7 +5677,7 @@ class HttpCli(object):
|
||||
|
||||
cur.connection.commit()
|
||||
if reload:
|
||||
self.conn.hsrv.broker.ask("reload", False, False).get()
|
||||
self.conn.hsrv.broker.ask("reload", False, True).get()
|
||||
self.conn.hsrv.broker.ask("up2k.wake_rescanner").get()
|
||||
|
||||
self.redirect("", "?shares")
|
||||
@@ -5734,7 +5769,7 @@ class HttpCli(object):
|
||||
cur.execute(q, (skey, fn))
|
||||
|
||||
cur.connection.commit()
|
||||
self.conn.hsrv.broker.ask("reload", False, False).get()
|
||||
self.conn.hsrv.broker.ask("reload", False, True).get()
|
||||
self.conn.hsrv.broker.ask("up2k.wake_rescanner").get()
|
||||
|
||||
fn = quotep(fns[0]) if len(fns) == 1 else ""
|
||||
|
||||
@@ -123,6 +123,7 @@ class HttpSrv(object):
|
||||
self.nm = NetMap([], [])
|
||||
self.ssdp: Optional["SSDPr"] = None
|
||||
self.gpwd = Garda(self.args.ban_pw)
|
||||
self.gpwc = Garda(self.args.ban_pwc)
|
||||
self.g404 = Garda(self.args.ban_404)
|
||||
self.g403 = Garda(self.args.ban_403)
|
||||
self.g422 = Garda(self.args.ban_422, False)
|
||||
|
||||
@@ -166,12 +166,13 @@ def au_unpk(
|
||||
znil = [x for x in znil if "cover" in x[0]] or znil
|
||||
znil = [x for x in znil if CBZ_01.search(x[0])] or znil
|
||||
t = "cbz: %d files, %d hits" % (nf, len(znil))
|
||||
using = sorted(znil)[0][1].filename
|
||||
if znil:
|
||||
t += ", using " + znil[0][1].filename
|
||||
t += ", using " + using
|
||||
log(t)
|
||||
if not znil:
|
||||
raise Exception("no images inside cbz")
|
||||
fi = zf.open(znil[0][1])
|
||||
fi = zf.open(using)
|
||||
|
||||
else:
|
||||
raise Exception("unknown compression %s" % (pk,))
|
||||
|
||||
@@ -320,7 +320,7 @@ class SMB(object):
|
||||
|
||||
self.hub.up2k.handle_mv(uname, "1.7.6.2", vp1, vp2)
|
||||
try:
|
||||
bos.makedirs(ap2)
|
||||
bos.makedirs(ap2, vfs2.flags["chmod_d"])
|
||||
except:
|
||||
pass
|
||||
|
||||
@@ -334,7 +334,7 @@ class SMB(object):
|
||||
t = "blocked mkdir (no-write-acc %s): /%s @%s"
|
||||
yeet(t % (vfs.axs.uwrite, vpath, uname))
|
||||
|
||||
return bos.mkdir(ap)
|
||||
return bos.mkdir(ap, vfs.flags["chmod_d"])
|
||||
|
||||
def _stat(self, vpath: str, *a: Any, **ka: Any) -> os.stat_result:
|
||||
try:
|
||||
|
||||
@@ -27,6 +27,7 @@ if True: # pylint: disable=using-constant-test
|
||||
|
||||
from .__init__ import ANYWIN, EXE, MACOS, PY2, TYPE_CHECKING, E, EnvParams, unicode
|
||||
from .authsrv import BAD_CFG, AuthSrv
|
||||
from .bos import bos
|
||||
from .cert import ensure_cert
|
||||
from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, HAVE_MUTAGEN
|
||||
from .pwhash import HAVE_ARGON2
|
||||
@@ -167,6 +168,7 @@ class SvcHub(object):
|
||||
# for non-http clients (ftp, tftp)
|
||||
self.bans: dict[str, int] = {}
|
||||
self.gpwd = Garda(self.args.ban_pw)
|
||||
self.gpwc = Garda(self.args.ban_pwc)
|
||||
self.g404 = Garda(self.args.ban_404)
|
||||
self.g403 = Garda(self.args.ban_403)
|
||||
self.g422 = Garda(self.args.ban_422, False)
|
||||
@@ -1118,7 +1120,7 @@ class SvcHub(object):
|
||||
|
||||
fn = sel_fn
|
||||
try:
|
||||
os.makedirs(os.path.dirname(fn))
|
||||
bos.makedirs(os.path.dirname(fn))
|
||||
except:
|
||||
pass
|
||||
|
||||
@@ -1135,6 +1137,9 @@ class SvcHub(object):
|
||||
|
||||
lh = codecs.open(fn, "w", encoding="utf-8", errors="replace")
|
||||
|
||||
if getattr(self.args, "free_umask", False):
|
||||
os.fchmod(lh.fileno(), 0o644)
|
||||
|
||||
argv = [pybin] + self.argv
|
||||
if hasattr(shlex, "quote"):
|
||||
argv = [shlex.quote(x) for x in argv]
|
||||
|
||||
@@ -282,7 +282,7 @@ class TcpSrv(object):
|
||||
except:
|
||||
pass # will create another ipv4 socket instead
|
||||
|
||||
if not ANYWIN and self.args.freebind:
|
||||
if getattr(self.args, "freebind", False):
|
||||
srv.setsockopt(socket.SOL_IP, socket.IP_FREEBIND, 1)
|
||||
|
||||
try:
|
||||
|
||||
@@ -387,14 +387,18 @@ class Tftpd(object):
|
||||
if not a:
|
||||
a = (self.args.iobuf,)
|
||||
|
||||
return open(ap, mode, *a, **ka)
|
||||
ret = open(ap, mode, *a, **ka)
|
||||
if wr and "chmod_f" in vfs.flags:
|
||||
os.fchmod(ret.fileno(), vfs.flags["chmod_f"])
|
||||
|
||||
return ret
|
||||
|
||||
def _mkdir(self, vpath: str, *a) -> None:
|
||||
vfs, _, ap = self._v2a("mkdir", vpath, [False, True])
|
||||
if "*" not in vfs.axs.uwrite:
|
||||
yeet("blocked mkdir; folder not world-writable: /%s" % (vpath,))
|
||||
|
||||
return bos.mkdir(ap)
|
||||
return bos.mkdir(ap, vfs.flags["chmod_d"])
|
||||
|
||||
def _unlink(self, vpath: str) -> None:
|
||||
# return bos.unlink(self._v2a("stat", vpath, *a)[1])
|
||||
|
||||
@@ -269,7 +269,8 @@ class ThumbSrv(object):
|
||||
self.log("joined waiting room for %r" % (tpath,))
|
||||
except:
|
||||
thdir = os.path.dirname(tpath)
|
||||
bos.makedirs(os.path.join(thdir, "w"))
|
||||
chmod = 0o700 if self.args.free_umask else 0o755
|
||||
bos.makedirs(os.path.join(thdir, "w"), chmod)
|
||||
|
||||
inf_path = os.path.join(thdir, "dir.txt")
|
||||
if not bos.path.exists(inf_path):
|
||||
@@ -284,7 +285,7 @@ class ThumbSrv(object):
|
||||
vn = next((x for x in allvols if x.realpath == ptop), None)
|
||||
if not vn:
|
||||
self.log("ptop %r not in %s" % (ptop, allvols), 3)
|
||||
vn = self.asrv.vfs.all_aps[0][1]
|
||||
vn = self.asrv.vfs.all_aps[0][1][0]
|
||||
|
||||
self.q.put((abspath, tpath, fmt, vn))
|
||||
self.log("conv %r :%s \033[0m%r" % (tpath, fmt, abspath), 6)
|
||||
|
||||
@@ -915,7 +915,8 @@ class Up2k(object):
|
||||
# only need to protect register_vpath but all in one go feels right
|
||||
for vol in vols:
|
||||
try:
|
||||
bos.makedirs(vol.realpath) # gonna happen at snap anyways
|
||||
# mkdir gonna happen at snap anyways;
|
||||
bos.makedirs(vol.realpath, vol.flags["chmod_d"])
|
||||
dir_is_empty(self.log_func, not self.args.no_scandir, vol.realpath)
|
||||
except Exception as ex:
|
||||
self.volstate[vol.vpath] = "OFFLINE (cannot access folder)"
|
||||
@@ -1141,6 +1142,20 @@ class Up2k(object):
|
||||
del fl[k1]
|
||||
else:
|
||||
fl[k1] = ",".join(x for x in fl[k1])
|
||||
|
||||
if fl["chmod_d"] == int(self.args.chmod_d, 8):
|
||||
fl.pop("chmod_d")
|
||||
try:
|
||||
if fl["chmod_f"] == int(self.args.chmod_f or "-1", 8):
|
||||
fl.pop("chmod_f")
|
||||
except:
|
||||
pass
|
||||
for k in ("chmod_f", "chmod_d"):
|
||||
try:
|
||||
fl[k] = "%o" % (fl[k])
|
||||
except:
|
||||
pass
|
||||
|
||||
a = [
|
||||
(ft if v is True else ff if v is False else fv).format(k, str(v))
|
||||
for k, v in fl.items()
|
||||
@@ -1364,6 +1379,10 @@ class Up2k(object):
|
||||
t = "volume /%s at [%s] is empty; will not be indexed as this could be due to an offline filesystem"
|
||||
self.log(t % (vol.vpath, rtop), 6)
|
||||
return True, False
|
||||
if not vol.check_landmarks():
|
||||
t = "volume /%s at [%s] will not be indexed due to bad landmarks"
|
||||
self.log(t % (vol.vpath, rtop), 6)
|
||||
return True, False
|
||||
|
||||
n_add, _, _ = self._build_dir(
|
||||
db,
|
||||
@@ -3290,7 +3309,7 @@ class Up2k(object):
|
||||
reg,
|
||||
"up2k._get_volsize",
|
||||
)
|
||||
bos.makedirs(ap2)
|
||||
bos.makedirs(ap2, vfs.flags["chmod_d"])
|
||||
vfs.lim.nup(cj["addr"])
|
||||
vfs.lim.bup(cj["addr"], cj["size"])
|
||||
|
||||
@@ -3397,11 +3416,11 @@ class Up2k(object):
|
||||
self.log(t % (mts - mtc, mts, mtc, fp))
|
||||
ow = False
|
||||
|
||||
ptop = job["ptop"]
|
||||
vf = self.flags.get(ptop) or {}
|
||||
if ow:
|
||||
self.log("replacing existing file at %r" % (fp,))
|
||||
cur = None
|
||||
ptop = job["ptop"]
|
||||
vf = self.flags.get(ptop) or {}
|
||||
st = bos.stat(fp)
|
||||
try:
|
||||
vrel = vjoin(job["prel"], fname)
|
||||
@@ -3421,8 +3440,13 @@ class Up2k(object):
|
||||
else:
|
||||
dip = self.hub.iphash.s(ip)
|
||||
|
||||
suffix = "-%.6f-%s" % (ts, dip)
|
||||
f, ret = ren_open(fname, "wb", fdir=fdir, suffix=suffix)
|
||||
f, ret = ren_open(
|
||||
fname,
|
||||
"wb",
|
||||
fdir=fdir,
|
||||
suffix="-%.6f-%s" % (ts, dip),
|
||||
chmod=vf.get("chmod_f", -1),
|
||||
)
|
||||
f.close()
|
||||
return ret
|
||||
|
||||
@@ -3452,6 +3476,8 @@ class Up2k(object):
|
||||
|
||||
linked = False
|
||||
try:
|
||||
if "reflink" in flags:
|
||||
raise Exception("reflink")
|
||||
if not is_mv and not flags.get("dedup"):
|
||||
raise Exception("dedup is disabled in config")
|
||||
|
||||
@@ -3508,7 +3534,8 @@ class Up2k(object):
|
||||
|
||||
linked = True
|
||||
except Exception as ex:
|
||||
self.log("cannot link; creating copy: " + repr(ex))
|
||||
if str(ex) != "reflink":
|
||||
self.log("cannot link; creating copy: " + repr(ex))
|
||||
if bos.path.isfile(src):
|
||||
csrc = src
|
||||
elif fsrc and bos.path.isfile(fsrc):
|
||||
@@ -4277,7 +4304,7 @@ class Up2k(object):
|
||||
self.log(t, 1)
|
||||
raise Pebkac(405, t)
|
||||
|
||||
bos.makedirs(os.path.dirname(dabs))
|
||||
bos.makedirs(os.path.dirname(dabs), dvn.flags["chmod_d"])
|
||||
|
||||
c1, w, ftime_, fsize_, ip, at = self._find_from_vpath(
|
||||
svn_dbv.realpath, srem_dbv
|
||||
@@ -4453,7 +4480,7 @@ class Up2k(object):
|
||||
vp = vjoin(dvp, rem)
|
||||
try:
|
||||
dvn, drem = self.vfs.get(vp, uname, False, True)
|
||||
bos.mkdir(dvn.canonical(drem))
|
||||
bos.mkdir(dvn.canonical(drem), dvn.flags["chmod_d"])
|
||||
except:
|
||||
pass
|
||||
|
||||
@@ -4523,7 +4550,7 @@ class Up2k(object):
|
||||
|
||||
is_xvol = svn.realpath != dvn.realpath
|
||||
|
||||
bos.makedirs(os.path.dirname(dabs))
|
||||
bos.makedirs(os.path.dirname(dabs), dvn.flags["chmod_d"])
|
||||
|
||||
if is_dirlink:
|
||||
dlabs = absreal(sabs)
|
||||
@@ -5030,8 +5057,13 @@ class Up2k(object):
|
||||
else:
|
||||
dip = self.hub.iphash.s(job["addr"])
|
||||
|
||||
suffix = "-%.6f-%s" % (job["t0"], dip)
|
||||
f, job["tnam"] = ren_open(tnam, "wb", fdir=pdir, suffix=suffix)
|
||||
f, job["tnam"] = ren_open(
|
||||
tnam,
|
||||
"wb",
|
||||
fdir=pdir,
|
||||
suffix="-%.6f-%s" % (job["t0"], dip),
|
||||
chmod=vf.get("chmod_f", -1),
|
||||
)
|
||||
try:
|
||||
abspath = djoin(pdir, job["tnam"])
|
||||
sprs = job["sprs"]
|
||||
|
||||
@@ -105,6 +105,7 @@ def _ens(want: str) -> tuple[int, ...]:
|
||||
# WSAENOTSOCK - no longer a socket
|
||||
# EUNATCH - can't assign requested address (wifi down)
|
||||
E_SCK = _ens("ENOTCONN EUNATCH EBADF WSAENOTSOCK WSAECONNRESET")
|
||||
E_SCK_WR = _ens("EPIPE ESHUTDOWN EBADFD")
|
||||
E_ADDR_NOT_AVAIL = _ens("EADDRNOTAVAIL WSAEADDRNOTAVAIL")
|
||||
E_ADDR_IN_USE = _ens("EADDRINUSE WSAEADDRINUSE")
|
||||
E_ACCESS = _ens("EACCES WSAEACCES")
|
||||
@@ -1584,6 +1585,7 @@ def ren_open(fname: str, *args: Any, **kwargs: Any) -> tuple[typing.IO[Any], str
|
||||
fun = kwargs.pop("fun", open)
|
||||
fdir = kwargs.pop("fdir", None)
|
||||
suffix = kwargs.pop("suffix", None)
|
||||
chmod = kwargs.pop("chmod", -1)
|
||||
|
||||
if fname == os.devnull:
|
||||
return fun(fname, *args, **kwargs), fname
|
||||
@@ -1627,6 +1629,11 @@ def ren_open(fname: str, *args: Any, **kwargs: Any) -> tuple[typing.IO[Any], str
|
||||
fp2 = os.path.join(fdir, fp2)
|
||||
with open(fsenc(fp2), "wb") as f2:
|
||||
f2.write(orig_name.encode("utf-8"))
|
||||
if chmod >= 0:
|
||||
os.fchmod(f2.fileno(), chmod)
|
||||
|
||||
if chmod >= 0:
|
||||
os.fchmod(f.fileno(), chmod)
|
||||
|
||||
return f, fname
|
||||
|
||||
@@ -1967,7 +1974,7 @@ def rand_name(fdir: str, fn: str, rnd: int) -> str:
|
||||
return fn
|
||||
|
||||
|
||||
def gen_filekey(alg: int, salt: str, fspath: str, fsize: int, inode: int) -> str:
|
||||
def _gen_filekey(alg: int, salt: str, fspath: str, fsize: int, inode: int) -> str:
|
||||
if alg == 1:
|
||||
zs = "%s %s %s %s" % (salt, fspath, fsize, inode)
|
||||
else:
|
||||
@@ -1977,6 +1984,13 @@ def gen_filekey(alg: int, salt: str, fspath: str, fsize: int, inode: int) -> str
|
||||
return ub64enc(hashlib.sha512(zb).digest()).decode("ascii")
|
||||
|
||||
|
||||
def _gen_filekey_w(alg: int, salt: str, fspath: str, fsize: int, inode: int) -> str:
|
||||
return _gen_filekey(alg, salt, fspath.replace("/", "\\"), fsize, inode)
|
||||
|
||||
|
||||
gen_filekey = _gen_filekey_w if ANYWIN else _gen_filekey
|
||||
|
||||
|
||||
def gen_filekey_dbg(
|
||||
alg: int,
|
||||
salt: str,
|
||||
@@ -2023,15 +2037,25 @@ def formatdate(ts: Optional[float] = None) -> str:
|
||||
return RFC2822 % (WKDAYS[wd], d, MONTHS[mo - 1], y, h, mi, s)
|
||||
|
||||
|
||||
def gencookie(k: str, v: str, r: str, tls: bool, dur: int = 0, txt: str = "") -> str:
|
||||
def gencookie(
|
||||
k: str, v: str, r: str, lax: bool, tls: bool, dur: int = 0, txt: str = ""
|
||||
) -> str:
|
||||
v = v.replace("%", "%25").replace(";", "%3B")
|
||||
if dur:
|
||||
exp = formatdate(time.time() + dur)
|
||||
else:
|
||||
exp = "Fri, 15 Aug 1997 01:00:00 GMT"
|
||||
|
||||
t = "%s=%s; Path=/%s; Expires=%s%s%s; SameSite=Lax"
|
||||
return t % (k, v, r, exp, "; Secure" if tls else "", txt)
|
||||
t = "%s=%s; Path=/%s; Expires=%s%s%s; SameSite=%s"
|
||||
return t % (
|
||||
k,
|
||||
v,
|
||||
r,
|
||||
exp,
|
||||
"; Secure" if tls else "",
|
||||
txt,
|
||||
"Lax" if lax else "Strict",
|
||||
)
|
||||
|
||||
|
||||
def humansize(sz: float, terse: bool = False) -> str:
|
||||
@@ -2400,11 +2424,11 @@ def pathmod(
|
||||
|
||||
# try to map abspath to vpath
|
||||
np = np.replace("/", os.sep)
|
||||
for vn_ap, vn in vfs.all_aps:
|
||||
for vn_ap, vns in vfs.all_aps:
|
||||
if not np.startswith(vn_ap):
|
||||
continue
|
||||
zs = np[len(vn_ap) :].replace(os.sep, "/")
|
||||
nvp = vjoin(vn.vpath, zs)
|
||||
nvp = vjoin(vns[0].vpath, zs)
|
||||
break
|
||||
|
||||
if nvp == "\n":
|
||||
|
||||
@@ -592,9 +592,7 @@ window.baguetteBox = (function () {
|
||||
preloadPrev(currentIndex);
|
||||
});
|
||||
|
||||
clmod(ebi('bbox-btns'), 'off');
|
||||
clmod(btnPrev, 'off');
|
||||
clmod(btnNext, 'off');
|
||||
show_buttons(0);
|
||||
|
||||
updateOffset();
|
||||
overlay.style.display = 'block';
|
||||
@@ -776,6 +774,8 @@ window.baguetteBox = (function () {
|
||||
if (is_vid) {
|
||||
image.volume = clamp(fcfg_get('vol', dvol / 100), 0, 1);
|
||||
image.setAttribute('controls', 'controls');
|
||||
image.setAttribute('playsinline', '1');
|
||||
// ios ignores poster
|
||||
image.onended = vidEnd;
|
||||
image.onplay = function () { show_buttons(1); };
|
||||
image.onpause = function () { show_buttons(); };
|
||||
|
||||
@@ -2035,6 +2035,9 @@ a.btn,
|
||||
font-family: 'scp', monospace, monospace;
|
||||
font-family: var(--font-mono), 'scp', monospace, monospace;
|
||||
}
|
||||
#hkhelp b {
|
||||
text-shadow: 1px 0 0 var(--fg), -1px 0 0 var(--fg), 0 -1px 0 var(--fg);
|
||||
}
|
||||
html.noscroll,
|
||||
html.noscroll .sbar {
|
||||
scrollbar-width: none;
|
||||
@@ -2196,18 +2199,25 @@ html.y #bbox-overlay figcaption a {
|
||||
top: calc(50% - 30px);
|
||||
width: 44px;
|
||||
height: 60px;
|
||||
transition: background-color .3s ease, color .3s ease, left .3s ease, right .3s ease;
|
||||
}
|
||||
#bbox-btns button {
|
||||
transition: background-color .3s ease, color .3s ease;
|
||||
}
|
||||
#bbox-btns {
|
||||
transition: top .3s ease;
|
||||
}
|
||||
.bbox-btn {
|
||||
position: fixed;
|
||||
}
|
||||
.bbox-btn,
|
||||
#bbox-btns {
|
||||
opacity: 1;
|
||||
animation: opacity .2s infinite ease-in-out;
|
||||
#bbox-next.off {
|
||||
right: -2.6em;
|
||||
}
|
||||
#bbox-prev.off {
|
||||
left: -2.6em;
|
||||
}
|
||||
.bbox-btn.off,
|
||||
#bbox-btns.off {
|
||||
opacity: 0;
|
||||
top: -2.2em;
|
||||
}
|
||||
#bbox-overlay button {
|
||||
cursor: pointer;
|
||||
@@ -2218,8 +2228,6 @@ html.y #bbox-overlay figcaption a {
|
||||
border-radius: 15%;
|
||||
background: rgba(50, 50, 50, 0.5);
|
||||
color: rgba(255,255,255,0.7);
|
||||
transition: background-color .3s ease;
|
||||
transition: color .3s ease;
|
||||
font-size: 1.4em;
|
||||
line-height: 1.4em;
|
||||
vertical-align: top;
|
||||
@@ -3264,4 +3272,9 @@ html.d #treepar {
|
||||
.dropdesc>div>div {
|
||||
transition: none;
|
||||
}
|
||||
#bbox-next,
|
||||
#bbox-prev,
|
||||
#bbox-btns {
|
||||
transition: background-color .3s ease, color .3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ var Ls = {
|
||||
"file-manager",
|
||||
["G", "toggle list / grid view"],
|
||||
["T", "toggle thumbnails / icons"],
|
||||
["🡅 A/D", "thumbnail size"],
|
||||
["⇧ A/D", "thumbnail size"],
|
||||
["ctrl-K", "delete selected"],
|
||||
["ctrl-X", "cut selection to clipboard"],
|
||||
["ctrl-C", "copy selection to clipboard"],
|
||||
@@ -45,9 +45,9 @@ var Ls = {
|
||||
|
||||
"file-list-sel",
|
||||
["space", "toggle file selection"],
|
||||
["🡑/🡓", "move selection cursor"],
|
||||
["ctrl 🡑/🡓", "move cursor and viewport"],
|
||||
["🡅 🡑/🡓", "select prev/next file"],
|
||||
["↑/↓", "move selection cursor"],
|
||||
["ctrl ↑/↓", "move cursor and viewport"],
|
||||
["⇧ ↑/↓", "select prev/next file"],
|
||||
["ctrl-A", "select all files / folders"],
|
||||
], [
|
||||
"navigation",
|
||||
@@ -70,7 +70,7 @@ var Ls = {
|
||||
["Home/End", "first/last pic"],
|
||||
["F", "fullscreen"],
|
||||
["R", "rotate clockwise"],
|
||||
["🡅 R", "rotate ccw"],
|
||||
["⇧ R", "rotate ccw"],
|
||||
["S", "select pic"],
|
||||
["Y", "download pic"],
|
||||
], [
|
||||
@@ -226,6 +226,7 @@ var Ls = {
|
||||
"ct_csel": 'use CTRL and SHIFT for file selection in grid-view">sel',
|
||||
"ct_ihop": 'when the image viewer is closed, scroll down to the last viewed file">g⮯',
|
||||
"ct_dots": 'show hidden files (if server permits)">dotfiles',
|
||||
"ct_qdel": 'when deleting files, only ask for confirmation once">qdel',
|
||||
"ct_dir1st": 'sort folders before files">📁 first',
|
||||
"ct_nsort": 'natural sort (for filenames with leading digits)">nsort',
|
||||
"ct_readme": 'show README.md in folder listings">📜 readme',
|
||||
@@ -277,6 +278,7 @@ var Ls = {
|
||||
"ml_drc": "dynamic range compressor",
|
||||
|
||||
"mt_loop": "loop/repeat one song\">🔁",
|
||||
"mt_one": "stop after one song\">1️⃣",
|
||||
"mt_shuf": "shuffle the songs in each folder\">🔀",
|
||||
"mt_aplay": "autoplay if there is a song-ID in the link you clicked to access the server$N$Ndisabling this will also stop the page URL from being updated with song-IDs when playing music, to prevent autoplay if these settings are lost but the URL remains\">a▶",
|
||||
"mt_preload": "start loading the next song near the end for gapless playback\">preload",
|
||||
@@ -294,6 +296,7 @@ var Ls = {
|
||||
"mt_uncache": "clear cache (try this if your browser cached$Na broken copy of a song so it refuses to play)\">uncache",
|
||||
"mt_mloop": "loop the open folder\">🔁 loop",
|
||||
"mt_mnext": "load the next folder and continue\">📂 next",
|
||||
"mt_mstop": "stop playback\">⏸ stop",
|
||||
"mt_cflac": "convert flac / wav to opus\">flac",
|
||||
"mt_caac": "convert aac / m4a to opus\">aac",
|
||||
"mt_coth": "convert all others (not mp3) to opus\">oth",
|
||||
@@ -658,7 +661,7 @@ var Ls = {
|
||||
"filbehandler",
|
||||
["G", "listevisning eller ikoner"],
|
||||
["T", "miniatyrbilder på/av"],
|
||||
["🡅 A/D", "ikonstørrelse"],
|
||||
["⇧ A/D", "ikonstørrelse"],
|
||||
["ctrl-K", "slett valgte"],
|
||||
["ctrl-X", "klipp ut valgte"],
|
||||
["ctrl-C", "kopiér til utklippstavle"],
|
||||
@@ -668,9 +671,9 @@ var Ls = {
|
||||
|
||||
"filmarkering",
|
||||
["space", "marker fil"],
|
||||
["🡑/🡓", "flytt markør"],
|
||||
["ctrl 🡑/🡓", "flytt markør og scroll"],
|
||||
["🡅 🡑/🡓", "velg forr./neste fil"],
|
||||
["↑/↓", "flytt markør"],
|
||||
["ctrl ↑/↓", "flytt markør og scroll"],
|
||||
["⇧ ↑/↓", "velg forr./neste fil"],
|
||||
["ctrl-A", "velg alle filer / mapper"],
|
||||
], [
|
||||
"navigering",
|
||||
@@ -693,7 +696,7 @@ var Ls = {
|
||||
["Home/End", "første/siste bilde"],
|
||||
["F", "fullskjermvisning"],
|
||||
["R", "rotere mot høyre"],
|
||||
["🡅 R", "rotere mot venstre"],
|
||||
["⇧ R", "rotere mot venstre"],
|
||||
["S", "marker bilde"],
|
||||
["Y", "last ned bilde"],
|
||||
], [
|
||||
@@ -850,6 +853,7 @@ var Ls = {
|
||||
"ct_csel": 'bruk tastene CTRL og SHIFT for markering av filer i ikonvisning">merk',
|
||||
"ct_ihop": 'bla ned til sist viste bilde når bildeviseren lukkes">g⮯',
|
||||
"ct_dots": 'vis skjulte filer (gitt at serveren tillater det)">.synlig',
|
||||
"ct_qdel": 'sletteknappen spør bare én gang om bekreftelse">hurtig🗑️',
|
||||
"ct_dir1st": 'sorter slik at mapper kommer foran filer">📁 først',
|
||||
"ct_nsort": 'naturlig sortering (forstår tall i filnavn)">nsort',
|
||||
"ct_readme": 'vis README.md nedenfor filene">📜 readme',
|
||||
@@ -901,6 +905,7 @@ var Ls = {
|
||||
"ml_drc": "compressor (volum-utjevning)",
|
||||
|
||||
"mt_loop": "spill den samme sangen om og om igjen\">🔁",
|
||||
"mt_one": "spill kun én sang\">1️⃣",
|
||||
"mt_shuf": "sangene i hver mappe$Nspilles i tilfeldig rekkefølge\">🔀",
|
||||
"mt_aplay": "forsøk å starte avspilling hvis linken du klikket på for å åpne nettsiden inneholder en sang-ID$N$Nhvis denne deaktiveres så vil heller ikke nettside-URLen bli oppdatert med sang-ID'er når musikk spilles, i tilfelle innstillingene skulle gå tapt og nettsiden lastes på ny\">a▶",
|
||||
"mt_preload": "hent ned litt av neste sang i forkant,$Nslik at pausen i overgangen blir mindre\">forles",
|
||||
@@ -918,6 +923,7 @@ var Ls = {
|
||||
"mt_uncache": "prøv denne hvis en sang ikke spiller riktig\">oppfrisk",
|
||||
"mt_mloop": "repeter hele mappen\">🔁 gjenta",
|
||||
"mt_mnext": "hopp til neste mappe og fortsett\">📂 neste",
|
||||
"mt_mstop": "stopp avspilling\">⏸ stopp",
|
||||
"mt_cflac": "konverter flac / wav-filer til opus\">flac",
|
||||
"mt_caac": "konverter aac / m4a-filer til to opus\">aac",
|
||||
"mt_coth": "konverter alt annet (men ikke mp3) til opus\">andre",
|
||||
@@ -1283,7 +1289,7 @@ var Ls = {
|
||||
"file-manager",
|
||||
["G", "切换列表 / 网格视图"],
|
||||
["T", "切换缩略图 / 图标"],
|
||||
["🡅 A/D", "缩略图大小"],
|
||||
["⇧ A/D", "缩略图大小"],
|
||||
["ctrl-K", "删除选中项"],
|
||||
["ctrl-X", "剪切选中项"],
|
||||
["ctrl-C", "复制选中项"], //m
|
||||
@@ -1293,9 +1299,9 @@ var Ls = {
|
||||
|
||||
"file-list-sel",
|
||||
["space", "切换文件选择"],
|
||||
["🡑/🡓", "移动选择光标"],
|
||||
["ctrl 🡑/🡓", "移动光标和视图"],
|
||||
["🡅 🡑/🡓", "选择上一个/下一个文件"],
|
||||
["↑/↓", "移动选择光标"],
|
||||
["ctrl ↑/↓", "移动光标和视图"],
|
||||
["⇧ ↑/↓", "选择上一个/下一个文件"],
|
||||
["ctrl-A", "选择所有文件 / 文件夹"]
|
||||
], [
|
||||
"navigation",
|
||||
@@ -1318,7 +1324,7 @@ var Ls = {
|
||||
["Home/End", "第一张/最后一张图片"],
|
||||
["F", "全屏"],
|
||||
["R", "顺时针旋转"],
|
||||
["🡅 R", "逆时针旋转"],
|
||||
["⇧ R", "逆时针旋转"],
|
||||
["S", "选择图片"], //m
|
||||
["Y", "下载图片"]
|
||||
], [
|
||||
@@ -1474,6 +1480,7 @@ var Ls = {
|
||||
"ct_csel": '在网格视图中使用 CTRL 和 SHIFT 进行文件选择">CTRL',
|
||||
"ct_ihop": '当图像查看器关闭时,滚动到最后查看的文件">滚动',
|
||||
"ct_dots": '显示隐藏文件(如果服务器允许)">隐藏文件',
|
||||
"ct_qdel": '删除文件时,只需确认一次">快删', //m
|
||||
"ct_dir1st": '在文件之前排序文件夹">📁 排序',
|
||||
"ct_nsort": '正确排序以数字开头的文件名">数字排序', //m
|
||||
"ct_readme": '在文件夹列表中显示 README.md">📜 readme',
|
||||
@@ -1525,6 +1532,7 @@ var Ls = {
|
||||
"ml_drc": "动态范围压缩器",
|
||||
|
||||
"mt_loop": "循环播放当前的歌曲\">🔁", //m
|
||||
"mt_one": "只播放一首歌后停止\">1️⃣", //m
|
||||
"mt_shuf": "在每个文件夹中随机播放歌曲\">🔀",
|
||||
"mt_aplay": "如果链接中有歌曲 ID,则自动播放,禁用此选项将停止在播放音乐时更新页面 URL 中的歌曲 ID,以防止在设置丢失但 URL 保留时自动播放\">自动播放▶",
|
||||
"mt_preload": "在歌曲快结束时开始加载下一首歌,以实现无缝播放\">预加载",
|
||||
@@ -1542,6 +1550,7 @@ var Ls = {
|
||||
"mt_uncache": "清除缓存 $N(如果你的浏览器缓存了一个损坏的歌曲副本而拒绝播放,请尝试此操作)\">uncache",
|
||||
"mt_mloop": "循环打开的文件夹\">🔁 循环",
|
||||
"mt_mnext": "加载下一个文件夹并继续\">📂 下一首",
|
||||
"mt_mstop": "停止播放\">⏸ 停止", //m
|
||||
"mt_cflac": "将 flac / wav 转换为 opus\">flac",
|
||||
"mt_caac": "将 aac / m4a 转换为 opus\">aac",
|
||||
"mt_coth": "将所有其他(不是 mp3)转换为 opus\">oth",
|
||||
@@ -2090,6 +2099,7 @@ ebi('op_cfg').innerHTML = (
|
||||
' <a id="csel" class="tgl btn" href="#" tt="' + L.ct_csel + '</a>\n' +
|
||||
' <a id="ihop" class="tgl btn" href="#" tt="' + L.ct_ihop + '</a>\n' +
|
||||
' <a id="dotfiles" class="tgl btn" href="#" tt="' + L.ct_dots + '</a>\n' +
|
||||
' <a id="qdel" class="tgl btn" href="#" tt="' + L.ct_qdel + '</a>\n' +
|
||||
' <a id="dir1st" class="tgl btn" href="#" tt="' + L.ct_dir1st + '</a>\n' +
|
||||
' <a id="nsort" class="tgl btn" href="#" tt="' + L.ct_nsort + '</a>\n' +
|
||||
' <a id="ireadme" class="tgl btn" href="#" tt="' + L.ct_readme + '</a>\n' +
|
||||
@@ -2362,6 +2372,7 @@ var mpl = (function () {
|
||||
ebi('op_player').innerHTML = (
|
||||
'<div><h3>' + L.cl_opts + '</h3><div>' +
|
||||
'<a href="#" class="tgl btn" id="au_loop" tt="' + L.mt_loop + '</a>' +
|
||||
'<a href="#" class="tgl btn" id="au_one" tt="' + L.mt_one + '</a>' +
|
||||
'<a href="#" class="tgl btn" id="au_shuf" tt="' + L.mt_shuf + '</a>' +
|
||||
'<a href="#" class="tgl btn" id="au_aplay" tt="' + L.mt_aplay + '</a>' +
|
||||
'<a href="#" class="tgl btn" id="au_preload" tt="' + L.mt_preload + '</a>' +
|
||||
@@ -2385,6 +2396,7 @@ var mpl = (function () {
|
||||
'<div><h3>' + L.ml_pmode + '</h3><div id="pb_mode">' +
|
||||
'<a href="#" class="tgl btn" m="loop" tt="' + L.mt_mloop + '</a>' +
|
||||
'<a href="#" class="tgl btn" m="next" tt="' + L.mt_mnext + '</a>' +
|
||||
'<a href="#" class="tgl btn" m="stop" tt="' + L.mt_mstop + '</a>' +
|
||||
'</div></div>' +
|
||||
|
||||
(have_acode ? (
|
||||
@@ -2410,11 +2422,15 @@ var mpl = (function () {
|
||||
'');
|
||||
|
||||
var r = {
|
||||
"pb_mode": (sread('pb_mode', ['loop', 'next']) || 'next').split('-')[0],
|
||||
"pb_mode": (sread('pb_mode', ['loop', 'next', 'stop']) || 'next').split('-')[0],
|
||||
"os_ctl": bcfg_get('au_os_ctl', have_mctl) && have_mctl,
|
||||
'traversals': 0,
|
||||
'm3ut': '#EXTM3U\n',
|
||||
};
|
||||
bcfg_bind(r, 'one', 'au_one', false, function (v) {
|
||||
if (mp.au)
|
||||
mp.au.loop = !v && r.loop;
|
||||
});
|
||||
bcfg_bind(r, 'loop', 'au_loop', false, function (v) {
|
||||
if (mp.au)
|
||||
mp.au.loop = v;
|
||||
@@ -3664,7 +3680,7 @@ var mpui = (function () {
|
||||
}
|
||||
|
||||
// preload next song
|
||||
if (mpl.preload && preloaded != mp.au.rsrc) {
|
||||
if (!mpl.one && mpl.preload && preloaded != mp.au.rsrc) {
|
||||
var len = mp.au.duration,
|
||||
rem = pos > 1 ? len - pos : 999,
|
||||
full = null;
|
||||
@@ -3683,7 +3699,12 @@ var mpui = (function () {
|
||||
var oi = mp.order.indexOf(mp.au.tid) + 1,
|
||||
evp = get_evpath();
|
||||
|
||||
if (mpl.pb_mode == 'loop' || mp.au.evp != evp)
|
||||
if (oi >= mp.order.length && (
|
||||
mpl.one ||
|
||||
mpl.pb_mode != 'next' ||
|
||||
mp.au.evp != evp ||
|
||||
ebi('unsearch'))
|
||||
)
|
||||
oi = 0;
|
||||
|
||||
if (oi >= mp.order.length) {
|
||||
@@ -4169,6 +4190,9 @@ function play(tid, is_ev, seek) {
|
||||
}
|
||||
|
||||
if (tn >= mp.order.length) {
|
||||
if (mpl.pb_mode == 'stop')
|
||||
return;
|
||||
|
||||
if (mpl.pb_mode == 'loop' || ebi('unsearch')) {
|
||||
tn = 0;
|
||||
}
|
||||
@@ -4253,7 +4277,7 @@ function play(tid, is_ev, seek) {
|
||||
|
||||
try {
|
||||
mp.nopause();
|
||||
mp.au.loop = mpl.loop;
|
||||
mp.au.loop = mpl.loop && !mpl.one;
|
||||
if (mpl.aplay || is_ev !== -1)
|
||||
mp.au.play();
|
||||
|
||||
@@ -4299,6 +4323,8 @@ function scroll2playing() {
|
||||
|
||||
|
||||
function evau_end(e) {
|
||||
if (mpl.one)
|
||||
return;
|
||||
if (!mpl.loop)
|
||||
return next_song(e);
|
||||
ev(e);
|
||||
@@ -5453,7 +5479,16 @@ var fileman = (function () {
|
||||
deleter();
|
||||
}
|
||||
|
||||
var asks = r.qdel ? 1 : 2;
|
||||
if (dqdel === 0)
|
||||
asks -= 1;
|
||||
|
||||
if (!asks)
|
||||
return deleter();
|
||||
|
||||
modal.confirm('<h6 style="color:#900">' + L.danger + '</h6>\n<b>' + L.fd_warn1.format(vps.length) + '</b><ul>' + uricom_adec(vps, true).join('') + '</ul>', function () {
|
||||
if (asks === 1)
|
||||
return deleter();
|
||||
modal.confirm(L.fd_warn2, deleter, null);
|
||||
}, null);
|
||||
};
|
||||
@@ -5814,6 +5849,8 @@ var fileman = (function () {
|
||||
r.bus.onmessage();
|
||||
};
|
||||
|
||||
bcfg_bind(r, 'qdel', 'qdel', dqdel == 1);
|
||||
|
||||
bren.onclick = r.rename;
|
||||
bdel.onclick = r.delete;
|
||||
bcut.onclick = r.cut;
|
||||
@@ -6009,6 +6046,7 @@ var showfile = (function () {
|
||||
m = /[?&](k=[^&#]+)/.exec(url);
|
||||
|
||||
url = url.split('?')[0] + (m ? '?' + m[1] : '');
|
||||
assert_vp(url);
|
||||
if (r.taildoc)
|
||||
return r.tail(url, no_push);
|
||||
|
||||
@@ -6885,8 +6923,10 @@ function hkhelp() {
|
||||
try {
|
||||
if (c[a].length != 2)
|
||||
html.push('<tr><th colspan="2">' + esc(c[a]) + '</th></tr>');
|
||||
else
|
||||
html.push('<tr><td>{0}</td><td>{1}</td></tr>'.format(c[a][0], c[a][1]));
|
||||
else {
|
||||
var t1 = c[a][0].replace('⇧', '<b>⇧</b>');
|
||||
html.push('<tr><td>{0}</td><td>{1}</td></tr>'.format(t1, c[a][1]));
|
||||
}
|
||||
}
|
||||
catch (ex) {
|
||||
html.push(">>> " + c[a]);
|
||||
@@ -7425,7 +7465,7 @@ var search_ui = (function () {
|
||||
nodes = ['<tr><td>-</td><td><div>' + links + '</div>', sz];
|
||||
|
||||
for (var b = 0; b < tagord.length; b++) {
|
||||
var k = tagord[b],
|
||||
var k = esc(tagord[b]),
|
||||
v = r.tags[k] || "";
|
||||
|
||||
if (k == ".dur") {
|
||||
@@ -7434,7 +7474,7 @@ var search_ui = (function () {
|
||||
continue;
|
||||
}
|
||||
|
||||
nodes.push(v);
|
||||
nodes.push(esc('' + v));
|
||||
}
|
||||
|
||||
nodes = nodes.concat([ext, unix2iso(ts)]);
|
||||
@@ -7501,6 +7541,7 @@ function ev_load_m3u(e) {
|
||||
return false;
|
||||
}
|
||||
function load_m3u(url) {
|
||||
assert_vp(url);
|
||||
var xhr = new XHR();
|
||||
xhr.open('GET', url, true);
|
||||
xhr.onload = render_m3u;
|
||||
@@ -8252,6 +8293,7 @@ var treectl = (function () {
|
||||
};
|
||||
|
||||
r.gentab = function (top, res) {
|
||||
showfile.untail();
|
||||
var nodes = res.dirs.concat(res.files),
|
||||
html = mk_files_header(res.taglist),
|
||||
sel = msel.hist[top],
|
||||
@@ -8322,7 +8364,7 @@ var treectl = (function () {
|
||||
top + tn.href + '" id="' + id + '">' + hname + '</a>', tn.sz];
|
||||
|
||||
for (var b = 0; b < res.taglist.length; b++) {
|
||||
var k = res.taglist[b],
|
||||
var k = esc(res.taglist[b]),
|
||||
v = (tn.tags || {})[k] || "",
|
||||
sv = null;
|
||||
|
||||
@@ -8331,7 +8373,7 @@ var treectl = (function () {
|
||||
else if (k == ".up_at")
|
||||
sv = v ? unix2iso(v) : "";
|
||||
else {
|
||||
ln.push(v);
|
||||
ln.push(esc('' + v));
|
||||
continue;
|
||||
}
|
||||
ln[ln.length - 1] += '</td><td sortv="' + v + '">' + sv;
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<span class="os lin mac">
|
||||
{% if accs %}<code><b id="pw0">{{ pw }}</b></code>=password, {% endif %}<code><b>mp</b></code>=mountpoint
|
||||
</span>
|
||||
<a href="#" id="setpw">use real password</a>
|
||||
{% if accs %}<a href="#" id="setpw">use real password</a>{% endif %}
|
||||
</p>
|
||||
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ function setos(os) {
|
||||
setos(WINDOWS ? 'win' : LINUX ? 'lin' : MACOS ? 'mac' : 'idk');
|
||||
|
||||
|
||||
ebi('setpw').onclick = function (e) {
|
||||
function setpw(e) {
|
||||
ev(e);
|
||||
modal.prompt('password:', '', function (v) {
|
||||
if (!v)
|
||||
@@ -57,7 +57,7 @@ ebi('setpw').onclick = function (e) {
|
||||
|
||||
var pw0 = ebi('pw0').innerHTML,
|
||||
oa = QSA('b');
|
||||
|
||||
|
||||
for (var a = 0; a < oa.length; a++)
|
||||
if (oa[a].innerHTML == pw0)
|
||||
oa[a].textContent = v;
|
||||
@@ -65,3 +65,5 @@ ebi('setpw').onclick = function (e) {
|
||||
add_dls();
|
||||
});
|
||||
}
|
||||
if (ebi('setpw'))
|
||||
ebi('setpw').onclick = setpw;
|
||||
|
||||
@@ -383,8 +383,10 @@ if (!String.prototype.format)
|
||||
});
|
||||
};
|
||||
|
||||
var have_URL = false;
|
||||
try {
|
||||
new URL('/a/', 'https://a.com/');
|
||||
have_URL = true;
|
||||
}
|
||||
catch (ex) {
|
||||
console.log('ie11 shim URL()');
|
||||
@@ -732,6 +734,16 @@ function makeSortable(table, cb) {
|
||||
}
|
||||
|
||||
|
||||
function assert_vp(path) {
|
||||
if (path.indexOf('//') + 1)
|
||||
throw 'nonlocal1: ' + path;
|
||||
|
||||
var o = window.location.origin;
|
||||
if (have_URL && (new URL(path, o)).origin != o)
|
||||
throw 'nonlocal2: ' + path;
|
||||
}
|
||||
|
||||
|
||||
function linksplit(rp, id) {
|
||||
var ret = [],
|
||||
apath = '/',
|
||||
|
||||
@@ -1,3 +1,115 @@
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2025-0727-2305 `v1.18.5` SECURITY: fix XSS in media tags
|
||||
|
||||
## ⚠️ ATTN: this release fixes an XSS vulnerability
|
||||
|
||||
[GHSA-9q4r-x2hj-jmvr](https://github.com/9001/copyparty/security/advisories/GHSA-9q4r-x2hj-jmvr), exploitable in two different ways, could let an attacker execute arbitrary javascript on other users:
|
||||
* either: tricking someone into clicking a malicious URL to load and execute javascript
|
||||
* or: uploading a malicious audio file to the server, affecting any successive visitors
|
||||
|
||||
so, with new and curious eyes on the project, we are starting off with a bang. Huge thanks to @altperfect for finding and reporting this earlier today.
|
||||
|
||||
## recent important news
|
||||
|
||||
* [v1.18.5 (2025-07-28)](https://github.com/9001/copyparty/releases/tag/v1.18.5) fixed XSS in display of media tags
|
||||
* [v1.15.0 (2024-09-08)](https://github.com/9001/copyparty/releases/tag/v1.15.0) changed upload deduplication to be default-disabled
|
||||
* [v1.14.3 (2024-08-30)](https://github.com/9001/copyparty/releases/tag/v1.14.3) fixed a bug that was introduced in v1.13.8 (2024-08-13); this bug could lead to **data loss** -- see the v1.14.3 release-notes for details
|
||||
|
||||
## 🧪 new features
|
||||
|
||||
* #214 option to stop playback after one song, and/or at end of folder 6bb27e60
|
||||
|
||||
## 🩹 bugfixes
|
||||
|
||||
* GHSA-9q4r-x2hj-jmvr 895880ae
|
||||
* block external m3u files 2228f81f
|
||||
* #202 the connect-page could show IP-address when it should have used hostnames/domains b0dec83a
|
||||
* scrolling locked after tailing a file and closing it creatively d197e754
|
||||
|
||||
## 🔧 other changes
|
||||
|
||||
* #189 the `SameSite` cookie parameter now defaults to `Strict`, increasing CSRF protection ca6d0b8d
|
||||
* new option `--cookie-lax` reverts to previous value `Lax`
|
||||
* docker: add FTPS support b4199847
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2025-0725-1841 `v1.18.4` Landmarks
|
||||
|
||||
## 🧪 new features
|
||||
|
||||
* #182 [Landmarks](https://github.com/9001/copyparty#database-location) edba7fff
|
||||
* detects that a storage backend is glitching out and disengage the up2k-database as a precaution
|
||||
* #183 quickdelete 21a96bcf
|
||||
* new togglebutton `qdel` in the UI which reduces the number of deletion confirmations by one
|
||||
* global-option `--qdel=0` which can bring it all the way to zero (good luck)
|
||||
|
||||
## 🩹 bugfixes
|
||||
|
||||
* fix unpost in recently created shares 2d322dd4
|
||||
* fix filekeys on windows df6d4df4
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2025-0721-2307 `v1.18.3` drop the umask
|
||||
|
||||
## 🧪 new features
|
||||
|
||||
* #181 the default chmod (unix-permissions) of new files and folders can now be changed 9921c43e
|
||||
* `--chmod-d` or volflag `chmod_d` sets directory permissions; default is 755
|
||||
* `--chmod-f` or volflag `chmod_f` sets file permissions; default is usually 644 (OS-defined)
|
||||
* see `--help-chmod` which explains the numbers
|
||||
|
||||
## 🩹 bugfixes
|
||||
|
||||
* #179 couldn't combine `--shr` (shares) and `--xvol` (symlink-guard) 0f0f8d90
|
||||
* #180 gallery buttons could still be clicked when faded-out 8c32b0e7
|
||||
* rss-feeds were slightly busted when combined with rp-loc (location-based proxying) 56d3bcf5
|
||||
* music-playback within search-results no longer jumps into the next folder at end-of-list 9bc4c5d2
|
||||
* video-playback on iOS now behaves like on all other platforms 78605d9a
|
||||
* (it would force-switch into fullscreen because that's their default)
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2025-0707-1419 `v1.18.2` idp-vol persistence
|
||||
|
||||
## 🧪 new features
|
||||
|
||||
* IdP-volumes can optionally be persisted across restarts d162502c
|
||||
* there is a UI to manage the cached users/groups 4f264a0a
|
||||
* only available to users listed in the new option `--idp-adm`
|
||||
* api for manually rescanning several volumes at once 42c199e7
|
||||
* `/some/path/?scan` does that one volume like before
|
||||
* `/any/path/?scan=/vol1,/another/vol2` rescans `/vol1` and `/another/vol2`
|
||||
* volflag to hide volume from listing in controlpanel fd7c71d6
|
||||
|
||||
## 🩹 bugfixes
|
||||
|
||||
* macos: fix confusing crash when blocked by [Little Snitch](https://www.obdev.at/products/littlesnitch/) bf11b2a4
|
||||
* unpost could break in some hairy reverseproxy setups 1b2d3985
|
||||
* copyparty32.exe: fix segfault on win7 c9fafb20
|
||||
* ui: fix navpane overlapping the scrollbar (still a bit jank but eh) 7ef6fd13
|
||||
* usb-eject: support all volume names ed908b98
|
||||
* docker: ensure clean slate deb6711b
|
||||
* fix up2k on ie11 d2714434
|
||||
|
||||
## 🔧 other changes
|
||||
|
||||
* update buildscript for keyfinder to support llvm 65c4e035
|
||||
* #175 add `python-magic` into the `iv` and `dj` docker flavors (thx @Morganamilo) 77274e9d
|
||||
* properly killed the experimental docker flavors to avoid confusion 8306e3d9
|
||||
* copyparty.exe: updated pillow 299cff3f f6be3905
|
||||
* avif support was removed to save 2 MiB
|
||||
|
||||
## 🌠 fun facts
|
||||
|
||||
* this release was slightly delayed due to a [norwegian traffic jam](https://a.ocv.me/pub/g/2025/07/PXL_20250706_143558381.jpg)
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2025-0622-0020 `v1.18.0` Logtail
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
version: '3'
|
||||
services:
|
||||
|
||||
copyparty:
|
||||
|
||||
@@ -161,7 +161,7 @@ symbol legend,
|
||||
| upload | █ | █ | █ | █ | █ | █ | █ | █ | █ | █ | ╱ | █ | █ |
|
||||
| parallel uploads | █ | | | █ | █ | | • | | █ | █ | █ | | █ |
|
||||
| resumable uploads | █ | | █ | | | | | | █ | █ | █ | ╱ | |
|
||||
| upload segmenting | █ | | | | | | | █ | █ | █ | █ | ╱ | █ |
|
||||
| upload segmenting | █ | | | █ | | | | █ | █ | █ | █ | ╱ | █ |
|
||||
| upload acceleration | █ | | | | | | | | █ | | █ | | |
|
||||
| upload verification | █ | | | █ | █ | | | | █ | | | | |
|
||||
| upload deduplication | █ | | | | █ | | | | █ | | | | |
|
||||
@@ -488,7 +488,7 @@ symbol legend,
|
||||
* ⚠️ [isolated on-disk file hierarchy] in per-user folders
|
||||
* not that bad, can probably be remedied with bindmounts or maybe symlinks
|
||||
* ⚠️ uploads not resumable / accelerated / integrity-checked
|
||||
* ⚠️ on cloudflare: max upload size 100 MiB
|
||||
* 🔵 uploads are segmented; no filesize limit, even on cloudflare
|
||||
* ⚠️ uploading small files is slow; `4` files per sec (copyparty does `670`/sec, 160x faster)
|
||||
* ⚠️ no write-only / upload-only folders
|
||||
* ⚠️ http/webdav only; no ftp, zeroconf
|
||||
|
||||
@@ -9,7 +9,7 @@ ENV XDG_CONFIG_HOME=/cfg
|
||||
|
||||
RUN apk --no-cache add !pyc \
|
||||
tzdata wget mimalloc2 mimalloc2-insecure \
|
||||
py3-jinja2 py3-argon2-cffi py3-pyzmq py3-pillow \
|
||||
py3-jinja2 py3-argon2-cffi py3-pyzmq py3-openssl py3-pillow \
|
||||
ffmpeg
|
||||
|
||||
COPY i/dist/copyparty-sfx.py innvikler.sh ./
|
||||
|
||||
@@ -12,7 +12,7 @@ COPY i/bin/mtag/audio-bpm.py /mtag/
|
||||
COPY i/bin/mtag/audio-key.py /mtag/
|
||||
RUN apk add -U !pyc \
|
||||
tzdata wget mimalloc2 mimalloc2-insecure \
|
||||
py3-jinja2 py3-argon2-cffi py3-pyzmq py3-pillow \
|
||||
py3-jinja2 py3-argon2-cffi py3-pyzmq py3-openssl py3-pillow \
|
||||
py3-pip py3-cffi \
|
||||
ffmpeg \
|
||||
py3-magic \
|
||||
|
||||
@@ -9,7 +9,7 @@ ENV XDG_CONFIG_HOME=/cfg
|
||||
|
||||
RUN apk --no-cache add !pyc \
|
||||
tzdata wget mimalloc2 mimalloc2-insecure \
|
||||
py3-jinja2 py3-argon2-cffi py3-pillow py3-mutagen
|
||||
py3-jinja2 py3-argon2-cffi py3-openssl py3-pillow py3-mutagen
|
||||
|
||||
COPY i/dist/copyparty-sfx.py innvikler.sh ./
|
||||
ADD base ./base
|
||||
|
||||
@@ -9,7 +9,7 @@ ENV XDG_CONFIG_HOME=/cfg
|
||||
|
||||
RUN apk add -U !pyc \
|
||||
tzdata wget mimalloc2 mimalloc2-insecure \
|
||||
py3-jinja2 py3-argon2-cffi py3-pyzmq py3-pillow \
|
||||
py3-jinja2 py3-argon2-cffi py3-pyzmq py3-openssl py3-pillow \
|
||||
py3-pip py3-cffi \
|
||||
ffmpeg \
|
||||
py3-magic \
|
||||
|
||||
@@ -7,7 +7,7 @@ import subprocess as sp
|
||||
|
||||
# to convert the copyparty --help to html, run this in xfce4-terminal @ 140x43:
|
||||
_ = r""""
|
||||
echo; for a in '' -bind -accounts -flags -handlers -hooks -urlform -exp -ls -dbd -pwhash -zm; do
|
||||
echo; for a in '' -bind -accounts -flags -handlers -hooks -urlform -exp -ls -dbd -chmod -pwhash -zm; do
|
||||
./copyparty-sfx.py --help$a 2>/dev/null; printf '\n\n\n%0139d\n\n\n'; done # xfce4-terminal @ 140x43
|
||||
"""
|
||||
# click [edit] => [select all]
|
||||
|
||||
@@ -23,7 +23,7 @@ exit 0
|
||||
|
||||
|
||||
# first open an infinitely wide console (this is why you own an ultrawide) and copypaste this into it:
|
||||
for a in '' -bind -accounts -flags -handlers -hooks -urlform -exp -ls -dbd -pwhash -zm; do
|
||||
for a in '' -bind -accounts -flags -handlers -hooks -urlform -exp -ls -dbd -chmod -pwhash -zm; do
|
||||
./copyparty-sfx.py --help$a 2>/dev/null; printf '\n\n\n%0255d\n\n\n'; done
|
||||
|
||||
# then copypaste all of the output by pressing ctrl-shift-a, ctrl-shift-c
|
||||
|
||||
@@ -121,7 +121,7 @@ var tl_browser = {
|
||||
"file-manager",
|
||||
["G", "toggle list / grid view"],
|
||||
["T", "toggle thumbnails / icons"],
|
||||
["🡅 A/D", "thumbnail size"],
|
||||
["⇧ A/D", "thumbnail size"],
|
||||
["ctrl-K", "delete selected"],
|
||||
["ctrl-X", "cut selection to clipboard"],
|
||||
["ctrl-C", "copy selection to clipboard"],
|
||||
@@ -131,9 +131,9 @@ var tl_browser = {
|
||||
|
||||
"file-list-sel",
|
||||
["space", "toggle file selection"],
|
||||
["🡑/🡓", "move selection cursor"],
|
||||
["ctrl 🡑/🡓", "move cursor and viewport"],
|
||||
["🡅 🡑/🡓", "select prev/next file"],
|
||||
["↑/↓", "move selection cursor"],
|
||||
["ctrl ↑/↓", "move cursor and viewport"],
|
||||
["⇧ ↑/↓", "select prev/next file"],
|
||||
["ctrl-A", "select all files / folders"],
|
||||
], [
|
||||
"navigation",
|
||||
@@ -156,7 +156,7 @@ var tl_browser = {
|
||||
["Home/End", "first/last pic"],
|
||||
["F", "fullscreen"],
|
||||
["R", "rotate clockwise"],
|
||||
["🡅 R", "rotate ccw"],
|
||||
["⇧ R", "rotate ccw"],
|
||||
["S", "select pic"],
|
||||
["Y", "download pic"],
|
||||
], [
|
||||
@@ -312,6 +312,7 @@ var tl_browser = {
|
||||
"ct_csel": 'use CTRL and SHIFT for file selection in grid-view">sel',
|
||||
"ct_ihop": 'when the image viewer is closed, scroll down to the last viewed file">g⮯',
|
||||
"ct_dots": 'show hidden files (if server permits)">dotfiles',
|
||||
"ct_qdel": 'when deleting files, only ask for confirmation once">qdel',
|
||||
"ct_dir1st": 'sort folders before files">📁 first',
|
||||
"ct_nsort": 'natural sort (for filenames with leading digits)">nsort',
|
||||
"ct_readme": 'show README.md in folder listings">📜 readme',
|
||||
@@ -363,6 +364,7 @@ var tl_browser = {
|
||||
"ml_drc": "dynamic range compressor",
|
||||
|
||||
"mt_loop": "loop/repeat one song\">🔁",
|
||||
"mt_one": "stop after one song\">1️⃣",
|
||||
"mt_shuf": "shuffle the songs in each folder\">🔀",
|
||||
"mt_aplay": "autoplay if there is a song-ID in the link you clicked to access the server$N$Ndisabling this will also stop the page URL from being updated with song-IDs when playing music, to prevent autoplay if these settings are lost but the URL remains\">a▶",
|
||||
"mt_preload": "start loading the next song near the end for gapless playback\">preload",
|
||||
@@ -380,6 +382,7 @@ var tl_browser = {
|
||||
"mt_uncache": "clear cache (try this if your browser cached$Na broken copy of a song so it refuses to play)\">uncache",
|
||||
"mt_mloop": "loop the open folder\">🔁 loop",
|
||||
"mt_mnext": "load the next folder and continue\">📂 next",
|
||||
"mt_mstop": "stop playback\">⏸ stop",
|
||||
"mt_cflac": "convert flac / wav to opus\">flac",
|
||||
"mt_caac": "convert aac / m4a to opus\">aac",
|
||||
"mt_coth": "convert all others (not mp3) to opus\">oth",
|
||||
|
||||
229
tests/test_shr.py
Normal file
229
tests/test_shr.py
Normal file
@@ -0,0 +1,229 @@
|
||||
#!/usr/bin/env python3
|
||||
# coding: utf-8
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import sqlite3
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from copyparty.__init__ import ANYWIN
|
||||
from copyparty.authsrv import AuthSrv
|
||||
from copyparty.httpcli import HttpCli
|
||||
from copyparty.util import absreal
|
||||
from tests import util as tu
|
||||
from tests.util import Cfg
|
||||
|
||||
|
||||
class TestShr(unittest.TestCase):
|
||||
def log(self, src, msg, c=0):
|
||||
m = "%s" % (msg,)
|
||||
if (
|
||||
"warning: filesystem-path does not exist:" in m
|
||||
or "you are sharing a system directory:" in m
|
||||
or "symlink-based deduplication is enabled" in m
|
||||
or m.startswith("hint: argument")
|
||||
):
|
||||
return
|
||||
|
||||
print(("[%s] %s" % (src, msg)).encode("ascii", "replace").decode("ascii"))
|
||||
|
||||
def assertLD(self, url, auth, els, edl):
|
||||
ls = self.ls(url, auth)
|
||||
self.assertEqual(ls[0], len(els) == 2)
|
||||
if not ls[0]:
|
||||
return
|
||||
a = [list(sorted(els[0])), list(sorted(els[1]))]
|
||||
b = [list(sorted(ls[1])), list(sorted(ls[2]))]
|
||||
self.assertEqual(a, b)
|
||||
|
||||
if edl is None:
|
||||
edl = els[1]
|
||||
can_dl = []
|
||||
for fn in b[1]:
|
||||
if fn == "a.db":
|
||||
continue
|
||||
furl = url + "/" + fn
|
||||
if auth:
|
||||
furl += "?pw=p1"
|
||||
h, zb = self.curl(furl, True)
|
||||
if h.startswith("HTTP/1.1 200 "):
|
||||
can_dl.append(fn)
|
||||
self.assertEqual(edl, can_dl)
|
||||
|
||||
def setUp(self):
|
||||
self.td = tu.get_ramdisk()
|
||||
td = os.path.join(self.td, "vfs")
|
||||
os.mkdir(td)
|
||||
os.chdir(td)
|
||||
os.mkdir("d1")
|
||||
os.mkdir("d2")
|
||||
os.mkdir("d2/d3")
|
||||
for zs in ("d1/f1", "d2/f2", "d2/d3/f3"):
|
||||
with open(zs, "wb") as f:
|
||||
f.write(zs.encode("utf-8"))
|
||||
for dst in ("d1", "d2", "d2/d3"):
|
||||
src, fn = zs.rsplit("/", 1)
|
||||
os.symlink(absreal(zs), dst + "/l" + fn[-1:])
|
||||
|
||||
db = sqlite3.connect("a.db")
|
||||
with db:
|
||||
zs = r"create table sh (k text, pw text, vp text, pr text, st int, un text, t0 int, t1 int)"
|
||||
db.execute(zs)
|
||||
db.close()
|
||||
|
||||
def tearDown(self):
|
||||
os.chdir(tempfile.gettempdir())
|
||||
shutil.rmtree(self.td)
|
||||
|
||||
def cinit(self):
|
||||
self.asrv = AuthSrv(self.args, self.log)
|
||||
self.conn = tu.VHttpConn(self.args, self.asrv, self.log, b"", True)
|
||||
|
||||
def test1(self):
|
||||
self.args = Cfg(
|
||||
a=["u1:p1"],
|
||||
v=["::A,u1", "d1:v1:A,u1", "d2/d3:d2/d3:A,u1"],
|
||||
shr="/shr/",
|
||||
shr1="shr/",
|
||||
shr_db="a.db",
|
||||
shr_v=False,
|
||||
)
|
||||
self.cinit()
|
||||
|
||||
self.assertLD("", True, [["d1", "d2", "v1"], ["a.db"]], [])
|
||||
self.assertLD("d1", True, [[], ["f1", "l1", "l2", "l3"]], None)
|
||||
self.assertLD("v1", True, [[], ["f1", "l1", "l2", "l3"]], None)
|
||||
self.assertLD("d2", True, [["d3"], ["f2", "l1", "l2", "l3"]], None)
|
||||
self.assertLD("d2/d3", True, [[], ["f3", "l1", "l2", "l3"]], None)
|
||||
self.assertLD("d3", True, [], [])
|
||||
|
||||
jt = {
|
||||
"k": "r",
|
||||
"vp": ["/"],
|
||||
"pw": "",
|
||||
"exp": "99",
|
||||
"perms": ["read"],
|
||||
}
|
||||
print(self.post_json("?pw=p1&share", jt)[1])
|
||||
jt = {
|
||||
"k": "d2",
|
||||
"vp": ["/d2/"],
|
||||
"pw": "",
|
||||
"exp": "99",
|
||||
"perms": ["read"],
|
||||
}
|
||||
print(self.post_json("?pw=p1&share", jt)[1])
|
||||
self.conn.shutdown()
|
||||
self.cinit()
|
||||
|
||||
self.assertLD("", True, [["d1", "d2", "v1"], ["a.db"]], [])
|
||||
self.assertLD("d1", True, [[], ["f1", "l1", "l2", "l3"]], None)
|
||||
self.assertLD("v1", True, [[], ["f1", "l1", "l2", "l3"]], None)
|
||||
self.assertLD("d2", True, [["d3"], ["f2", "l1", "l2", "l3"]], None)
|
||||
self.assertLD("d2/d3", True, [[], ["f3", "l1", "l2", "l3"]], None)
|
||||
self.assertLD("d3", True, [], [])
|
||||
|
||||
self.assertLD("shr/d2", False, [[], ["f2", "l1", "l2", "l3"]], None)
|
||||
self.assertLD("shr/d2/d3", False, [], None)
|
||||
|
||||
self.assertLD("shr/r", False, [["d1"], ["a.db"]], [])
|
||||
self.assertLD("shr/r/d1", False, [[], ["f1", "l1", "l2", "l3"]], None)
|
||||
self.assertLD("shr/r/d2", False, [], None) # unfortunate
|
||||
self.assertLD("shr/r/d2/d3", False, [], None)
|
||||
|
||||
self.conn.shutdown()
|
||||
|
||||
def test2(self):
|
||||
self.args = Cfg(
|
||||
a=["u1:p1"],
|
||||
v=["::A,u1", "d1:v1:A,u1", "d2/d3:d2/d3:A,u1"],
|
||||
shr="/shr/",
|
||||
shr1="shr/",
|
||||
shr_db="a.db",
|
||||
shr_v=False,
|
||||
xvol=True,
|
||||
)
|
||||
self.cinit()
|
||||
|
||||
self.assertLD("", True, [["d1", "d2", "v1"], ["a.db"]], [])
|
||||
self.assertLD("d1", True, [[], ["f1", "l1", "l2", "l3"]], None)
|
||||
self.assertLD("v1", True, [[], ["f1", "l1", "l2", "l3"]], None)
|
||||
self.assertLD("d2", True, [["d3"], ["f2", "l1", "l2", "l3"]], None)
|
||||
self.assertLD("d2/d3", True, [[], ["f3", "l1", "l2", "l3"]], None)
|
||||
self.assertLD("d3", True, [], [])
|
||||
|
||||
jt = {
|
||||
"k": "r",
|
||||
"vp": ["/"],
|
||||
"pw": "",
|
||||
"exp": "99",
|
||||
"perms": ["read"],
|
||||
}
|
||||
print(self.post_json("?pw=p1&share", jt)[1])
|
||||
jt = {
|
||||
"k": "d2",
|
||||
"vp": ["/d2/"],
|
||||
"pw": "",
|
||||
"exp": "99",
|
||||
"perms": ["read"],
|
||||
}
|
||||
print(self.post_json("?pw=p1&share", jt)[1])
|
||||
self.conn.shutdown()
|
||||
self.cinit()
|
||||
|
||||
self.assertLD("", True, [["d1", "d2", "v1"], ["a.db"]], [])
|
||||
self.assertLD("d1", True, [[], ["f1", "l1", "l2", "l3"]], None)
|
||||
self.assertLD("v1", True, [[], ["f1", "l1", "l2", "l3"]], None)
|
||||
self.assertLD("d2", True, [["d3"], ["f2", "l1", "l2", "l3"]], None)
|
||||
self.assertLD("d2/d3", True, [[], ["f3", "l1", "l2", "l3"]], None)
|
||||
self.assertLD("d3", True, [], [])
|
||||
|
||||
self.assertLD("shr/d2", False, [[], ["f2", "l1", "l2", "l3"]], ["f2", "l2"])
|
||||
self.assertLD("shr/d2/d3", False, [], [])
|
||||
|
||||
self.assertLD("shr/r", False, [["d1"], ["a.db"]], [])
|
||||
self.assertLD(
|
||||
"shr/r/d1", False, [[], ["f1", "l1", "l2", "l3"]], ["f1", "l1", "l2"]
|
||||
)
|
||||
self.assertLD("shr/r/d2", False, [], []) # unfortunate
|
||||
self.assertLD("shr/r/d2/d3", False, [], [])
|
||||
|
||||
self.conn.shutdown()
|
||||
|
||||
def ls(self, url: str, auth: bool):
|
||||
zs = url + "?ls" + ("&pw=p1" if auth else "")
|
||||
h, b = self.curl(zs)
|
||||
if not h.startswith("HTTP/1.1 200 "):
|
||||
return (False, [], [])
|
||||
jo = json.loads(b)
|
||||
return (
|
||||
True,
|
||||
[x["href"].rstrip("/") for x in jo.get("dirs") or {}],
|
||||
[x["href"] for x in jo.get("files") or {}],
|
||||
)
|
||||
|
||||
def curl(self, url: str, binary=False):
|
||||
h = "GET /%s HTTP/1.1\r\nConnection: close\r\n\r\n"
|
||||
HttpCli(self.conn.setbuf((h % (url,)).encode("utf-8"))).run()
|
||||
if binary:
|
||||
h, b = self.conn.s._reply.split(b"\r\n\r\n", 1)
|
||||
return [h.decode("utf-8"), b]
|
||||
|
||||
return self.conn.s._reply.decode("utf-8").split("\r\n\r\n", 1)
|
||||
|
||||
def post_json(self, url: str, data):
|
||||
buf = json.dumps(data).encode("utf-8")
|
||||
msg = [
|
||||
"POST /%s HTTP/1.1" % (url,),
|
||||
"Connection: close",
|
||||
"Content-Type: application/json",
|
||||
"Content-Length: %d" % (len(buf),),
|
||||
"\r\n",
|
||||
]
|
||||
buf = "\r\n".join(msg).encode("utf-8") + buf
|
||||
print("PUT -->", buf)
|
||||
HttpCli(self.conn.setbuf(buf)).run()
|
||||
return self.conn.s._reply.decode("utf-8").split("\r\n\r\n", 1)
|
||||
@@ -143,7 +143,7 @@ class Cfg(Namespace):
|
||||
def __init__(self, a=None, v=None, c=None, **ka0):
|
||||
ka = {}
|
||||
|
||||
ex = "chpw daw dav_auth dav_mac dav_rt e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp early_ban ed emp exp force_js getmod grid gsel hardlink hardlink_only ih ihead magic nid nih no_acode no_athumb no_bauth no_clone no_cp no_dav no_db_ip no_del no_dirsz no_dupe no_lifetime no_logues no_mv no_pipe no_poll no_readme no_robots no_sb_md no_sb_lg no_scandir no_tail no_tarcmp no_thumb no_vthumb no_zip nrand nsort nw og og_no_head og_s_title ohead q rand re_dirsz rmagic rss smb srch_dbg srch_excl stats uqe vague_403 vc ver wo_up_readme write_uplog xdev xlink xvol zipmaxu zs"
|
||||
ex = "chpw cookie_lax daw dav_auth dav_mac dav_rt e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp early_ban ed emp exp force_js getmod grid gsel hardlink hardlink_only ih ihead magic nid nih no_acode no_athumb no_bauth no_clone no_cp no_dav no_db_ip no_del no_dirsz no_dupe no_lifetime no_logues no_mv no_pipe no_poll no_readme no_robots no_sb_md no_sb_lg no_scandir no_tail no_tarcmp no_thumb no_vthumb no_zip nrand nsort nw og og_no_head og_s_title ohead q rand re_dirsz reflink rmagic rss smb srch_dbg srch_excl stats uqe vague_403 vc ver wo_up_readme write_uplog xdev xlink xvol zipmaxu zs"
|
||||
ka.update(**{k: False for k in ex.split()})
|
||||
|
||||
ex = "dav_inf dedup dotpart dotsrch hook_v no_dhash no_fastboot no_fpool no_htp no_rescan no_sendfile no_ses no_snap no_up_list no_voldump re_dhash see_dots plain_ip"
|
||||
@@ -152,7 +152,7 @@ class Cfg(Namespace):
|
||||
ex = "ah_cli ah_gen css_browser dbpath hist ipu js_browser js_other mime mimes no_forget no_hash no_idx nonsus_urls og_tpl og_ua ua_nodoc ua_nozip"
|
||||
ka.update(**{k: None for k in ex.split()})
|
||||
|
||||
ex = "hash_mt hsortn safe_dedup srch_time tail_fd tail_rate u2abort u2j u2sz"
|
||||
ex = "hash_mt hsortn qdel safe_dedup srch_time tail_fd tail_rate u2abort u2j u2sz"
|
||||
ka.update(**{k: 1 for k in ex.split()})
|
||||
|
||||
ex = "au_vol dl_list mtab_age reg_cap s_thead s_tbody tail_tmax tail_who th_convt ups_who zip_who"
|
||||
@@ -161,10 +161,10 @@ class Cfg(Namespace):
|
||||
ex = "db_act forget_ip idp_store k304 loris no304 nosubtle re_maxage rproxy rsp_jtr rsp_slp s_wr_slp snap_wri theme themes turbo u2ow zipmaxn zipmaxs"
|
||||
ka.update(**{k: 0 for k in ex.split()})
|
||||
|
||||
ex = "ah_alg bname chpw_db doctitle df exit favico idp_h_usr ipa html_head lg_sba lg_sbf log_fk md_sba md_sbf name og_desc og_site og_th og_title og_title_a og_title_v og_title_i shr tcolor textfiles unlist vname xff_src zipmaxt R RS SR"
|
||||
ex = "ah_alg bname chmod_f chpw_db doctitle df exit favico idp_h_usr ipa html_head lg_sba lg_sbf log_fk md_sba md_sbf name og_desc og_site og_th og_title og_title_a og_title_v og_title_i shr tcolor textfiles unlist vname xff_src zipmaxt R RS SR"
|
||||
ka.update(**{k: "" for k in ex.split()})
|
||||
|
||||
ex = "ban_403 ban_404 ban_422 ban_pw ban_url spinner"
|
||||
ex = "ban_403 ban_404 ban_422 ban_pw ban_pwc ban_url spinner"
|
||||
ka.update(**{k: "no" for k in ex.split()})
|
||||
|
||||
ex = "ext_th grp on403 on404 xac xad xar xau xban xbc xbd xbr xbu xiu xm"
|
||||
@@ -181,6 +181,7 @@ class Cfg(Namespace):
|
||||
c=c,
|
||||
E=E,
|
||||
bup_ck="sha512",
|
||||
chmod_d="755",
|
||||
dbd="wal",
|
||||
dk_salt="b" * 16,
|
||||
fk_salt="a" * 16,
|
||||
@@ -260,6 +261,9 @@ class VHub(object):
|
||||
self.is_dut = True
|
||||
self.up2k = Up2k(self)
|
||||
|
||||
def reload(self, a, b):
|
||||
pass
|
||||
|
||||
|
||||
class VBrokerThr(BrokerThr):
|
||||
def __init__(self, hub):
|
||||
|
||||
Reference in New Issue
Block a user