Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ecced0c4f2 | ||
|
|
d4a8071de5 | ||
|
|
261236e302 | ||
|
|
0de09860f6 | ||
|
|
bfb39969a4 | ||
|
|
256dad8cc0 | ||
|
|
a247ba9ca3 | ||
|
|
0a9a807772 | ||
|
|
41fa6b2552 | ||
|
|
f425ff51ae | ||
|
|
7cde9a2976 | ||
|
|
5dcd88a6c8 | ||
|
|
c3ef3fdc1f | ||
|
|
b9ba783c1c | ||
|
|
d1bca1f52f | ||
|
|
94352f278b | ||
|
|
4fb87ebe32 | ||
|
|
3cbb7243ab |
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -34,7 +34,7 @@ remove the ones that are not relevant:
|
||||
### Server details (if you're NOT using docker/podman)
|
||||
remove the ones that are not relevant:
|
||||
* **server OS / version:**
|
||||
* **what copyparty did you grab:** (sfx/exe/pip/aur/...)
|
||||
* **what copyparty did you grab:** (sfx/exe/pip/arch/...)
|
||||
* **how you're running it:** (in a terminal, as a systemd-service, ...)
|
||||
* run copyparty with `--version` and grab the last 3 lines (they start with `copyparty`, `CPython`, `sqlite`) and paste them below this line:
|
||||
* **copyparty arguments and/or config-file:**
|
||||
|
||||
12
README.md
12
README.md
@@ -104,7 +104,7 @@ turn almost any device into a file server with resumable uploads/downloads using
|
||||
* [feature chickenbits](#feature-chickenbits) - buggy feature? rip it out
|
||||
* [feature beefybits](#feature-beefybits) - force-enable features with known issues on your OS/env
|
||||
* [packages](#packages) - the party might be closer than you think
|
||||
* [arch package](#arch-package) - now [available on aur](https://aur.archlinux.org/packages/copyparty) maintained by [@icxes](https://github.com/icxes)
|
||||
* [arch package](#arch-package) - `pacman -S copyparty` (in [arch linux extra](https://archlinux.org/packages/extra/any/copyparty/))
|
||||
* [fedora package](#fedora-package) - does not exist yet
|
||||
* [nix package](#nix-package) - `nix profile install github:9001/copyparty`
|
||||
* [nixos module](#nixos-module)
|
||||
@@ -417,6 +417,9 @@ upgrade notes
|
||||
|
||||
"frequently" asked questions
|
||||
|
||||
* CopyParty?
|
||||
* nope! the name is either copyparty (all-lowercase) or Copyparty -- it's [one word](https://en.wiktionary.org/wiki/copyparty) after all :>
|
||||
|
||||
* can I change the 🌲 spinning pine-tree loading animation?
|
||||
* [yeah...](https://github.com/9001/copyparty/tree/hovudstraum/docs/rice#boring-loader-spinner) :-(
|
||||
|
||||
@@ -915,6 +918,7 @@ semi-intentional limitations:
|
||||
|
||||
* cleanup of expired shares only works when global option `e2d` is set, and/or at least one volume on the server has volflag `e2d`
|
||||
* only folders from the same volume are shared; if you are sharing a folder which contains other volumes, then the contents of those volumes will not be available
|
||||
* if you change [password hashing](#password-hashing) settings after creating a password-protected share, then that share will stop working
|
||||
* related to [IdP volumes being forgotten on shutdown](https://github.com/9001/copyparty/blob/hovudstraum/docs/idp.md#idp-volumes-are-forgotten-on-shutdown), any shares pointing into a user's IdP volume will be unavailable until that user makes their first request after a restart
|
||||
* no option to "delete after first access" because tricky
|
||||
* when linking something to discord (for example) it'll get accessed by their scraper and that would count as a hit
|
||||
@@ -2203,10 +2207,14 @@ if your distro/OS is not mentioned below, there might be some hints in the [«on
|
||||
|
||||
## arch package
|
||||
|
||||
now [available on aur](https://aur.archlinux.org/packages/copyparty) maintained by [@icxes](https://github.com/icxes)
|
||||
`pacman -S copyparty` (in [arch linux extra](https://archlinux.org/packages/extra/any/copyparty/))
|
||||
|
||||
it comes with a [systemd service](./contrib/package/arch/copyparty.service) and expects to find one or more [config files](./docs/example.conf) in `/etc/copyparty.d/`
|
||||
|
||||
after installing it, you may want to `cp /usr/lib/systemd/system/copyparty.service /etc/systemd/system/` and then `vim /etc/systemd/system/copyparty.service` to change what user/group it is running as (you only need to do this once)
|
||||
|
||||
NOTE: there used to be an aur package; this evaporated when copyparty was adopted by the official archlinux repos. If you're still using the aur package, please move
|
||||
|
||||
|
||||
## fedora package
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ example usage as a volflag in a copyparty config file:
|
||||
### CONFIG
|
||||
|
||||
# filetypes to process; ignores everything else
|
||||
EXTS = "mp3 flac ogg opus m4a aac wav wma"
|
||||
EXTS = "mp3 flac ogg oga opus m4a aac wav wma"
|
||||
|
||||
# the name of the subdir to put the normalized files in
|
||||
SUBDIR = "normalized"
|
||||
|
||||
@@ -71,6 +71,9 @@ def main():
|
||||
## selecting it inside the print at the end:
|
||||
##
|
||||
|
||||
# move all uploads to one specific folder
|
||||
into_junk = {"vp": "/junk"}
|
||||
|
||||
# create a subfolder named after the filetype and move it into there
|
||||
into_subfolder = {"vp": ext}
|
||||
|
||||
@@ -92,8 +95,8 @@ def main():
|
||||
by_category = {} # no action
|
||||
|
||||
# now choose the default effect to apply; can be any of these:
|
||||
# into_subfolder into_toplevel into_sibling by_category
|
||||
effect = {"vp": "/junk"}
|
||||
# into_junk into_subfolder into_toplevel into_sibling by_category
|
||||
effect = into_sibling
|
||||
|
||||
##
|
||||
## but we can keep going, adding more speicifc rules
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
S_VERSION = "2.10"
|
||||
S_BUILD_DT = "2025-02-19"
|
||||
S_VERSION = "2.11"
|
||||
S_BUILD_DT = "2025-05-18"
|
||||
|
||||
"""
|
||||
u2c.py: upload to copyparty
|
||||
@@ -1289,7 +1289,7 @@ class Ctl(object):
|
||||
if self.ar.jw:
|
||||
print("%s %s" % (wark, vp))
|
||||
else:
|
||||
zd = datetime.datetime.fromtimestamp(file.lmod, UTC)
|
||||
zd = datetime.datetime.fromtimestamp(max(0, file.lmod), UTC)
|
||||
dt = "%04d-%02d-%02d %02d:%02d:%02d" % (
|
||||
zd.year,
|
||||
zd.month,
|
||||
|
||||
@@ -1,29 +1,31 @@
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
with lib; let
|
||||
mkKeyValue = key: value:
|
||||
if value == true then
|
||||
# sets with a true boolean value are coerced to just the key name
|
||||
if value == true
|
||||
then
|
||||
# sets with a true boolean value are coerced to just the key name
|
||||
key
|
||||
else if value == false then
|
||||
# or omitted completely when false
|
||||
else if value == false
|
||||
then
|
||||
# or omitted completely when false
|
||||
""
|
||||
else
|
||||
(generators.mkKeyValueDefault { inherit mkValueString; } ": " key value);
|
||||
else (generators.mkKeyValueDefault {inherit mkValueString;} ": " key value);
|
||||
|
||||
mkAttrsString = value: (generators.toKeyValue { inherit mkKeyValue; } value);
|
||||
mkAttrsString = value: (generators.toKeyValue {inherit mkKeyValue;} value);
|
||||
|
||||
mkValueString = value:
|
||||
if isList value then
|
||||
(concatStringsSep ", " (map mkValueString value))
|
||||
else if isAttrs value then
|
||||
"\n" + (mkAttrsString value)
|
||||
else
|
||||
(generators.mkValueStringDefault { } value);
|
||||
if isList value
|
||||
then (concatStringsSep ", " (map mkValueString value))
|
||||
else if isAttrs value
|
||||
then "\n" + (mkAttrsString value)
|
||||
else (generators.mkValueStringDefault {} value);
|
||||
|
||||
mkSectionName = value: "[" + (escape [ "[" "]" ] value) + "]";
|
||||
mkSectionName = value: "[" + (escape ["[" "]"] value) + "]";
|
||||
|
||||
mkSection = name: attrs: ''
|
||||
${mkSectionName name}
|
||||
@@ -49,12 +51,12 @@ let
|
||||
${concatStringsSep "\n" (mapAttrsToList mkVolume cfg.volumes)}
|
||||
'';
|
||||
|
||||
name = "copyparty";
|
||||
cfg = config.services.copyparty;
|
||||
configFile = pkgs.writeText "${name}.conf" configStr;
|
||||
runtimeConfigPath = "/run/${name}/${name}.conf";
|
||||
home = "/var/lib/${name}";
|
||||
defaultShareDir = "${home}/data";
|
||||
configFile = pkgs.writeText "copyparty.conf" configStr;
|
||||
runtimeConfigPath = "/run/copyparty/copyparty.conf";
|
||||
externalCacheDir = "/var/cache/copyparty";
|
||||
externalStateDir = "/var/lib/copyparty";
|
||||
defaultShareDir = "${externalStateDir}/data";
|
||||
in {
|
||||
options.services.copyparty = {
|
||||
enable = mkEnableOption "web-based file manager";
|
||||
@@ -68,6 +70,35 @@ in {
|
||||
'';
|
||||
};
|
||||
|
||||
mkHashWrapper = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = ''
|
||||
Make a shell script wrapper called 'copyparty-hash' with all options set here,
|
||||
that launches the hashing cli.
|
||||
'';
|
||||
};
|
||||
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
default = "copyparty";
|
||||
description = ''
|
||||
The user that copyparty will run under.
|
||||
|
||||
If changed from default, you are responsible for making sure the user exists.
|
||||
'';
|
||||
};
|
||||
|
||||
group = mkOption {
|
||||
type = types.str;
|
||||
default = "copyparty";
|
||||
description = ''
|
||||
The group that copyparty will run under.
|
||||
|
||||
If changed from default, you are responsible for making sure the user exists.
|
||||
'';
|
||||
};
|
||||
|
||||
openFilesLimit = mkOption {
|
||||
default = 4096;
|
||||
type = types.either types.int types.str;
|
||||
@@ -79,22 +110,25 @@ in {
|
||||
description = ''
|
||||
Global settings to apply.
|
||||
Directly maps to values in the [global] section of the copyparty config.
|
||||
Cannot set "c" or "hist", those are set by this module.
|
||||
See `${getExe cfg.package} --help` for more details.
|
||||
'';
|
||||
default = {
|
||||
i = "127.0.0.1";
|
||||
no-reload = true;
|
||||
hist = externalCacheDir;
|
||||
};
|
||||
example = literalExpression ''
|
||||
{
|
||||
i = "0.0.0.0";
|
||||
no-reload = true;
|
||||
hist = ${externalCacheDir};
|
||||
}
|
||||
'';
|
||||
};
|
||||
|
||||
accounts = mkOption {
|
||||
type = types.attrsOf (types.submodule ({ ... }: {
|
||||
type = types.attrsOf (types.submodule ({...}: {
|
||||
options = {
|
||||
passwordFile = mkOption {
|
||||
type = types.str;
|
||||
@@ -109,7 +143,7 @@ in {
|
||||
description = ''
|
||||
A set of copyparty accounts to create.
|
||||
'';
|
||||
default = { };
|
||||
default = {};
|
||||
example = literalExpression ''
|
||||
{
|
||||
ed.passwordFile = "/run/keys/copyparty/ed";
|
||||
@@ -118,10 +152,10 @@ in {
|
||||
};
|
||||
|
||||
volumes = mkOption {
|
||||
type = types.attrsOf (types.submodule ({ ... }: {
|
||||
type = types.attrsOf (types.submodule ({...}: {
|
||||
options = {
|
||||
path = mkOption {
|
||||
type = types.str;
|
||||
type = types.path;
|
||||
description = ''
|
||||
Path of a directory to share.
|
||||
'';
|
||||
@@ -177,7 +211,7 @@ in {
|
||||
nohash = "\.iso$";
|
||||
};
|
||||
'';
|
||||
default = { };
|
||||
default = {};
|
||||
};
|
||||
};
|
||||
}));
|
||||
@@ -185,7 +219,7 @@ in {
|
||||
default = {
|
||||
"/" = {
|
||||
path = defaultShareDir;
|
||||
access = { r = "*"; };
|
||||
access = {r = "*";};
|
||||
};
|
||||
};
|
||||
example = literalExpression ''
|
||||
@@ -204,52 +238,65 @@ in {
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
config = mkIf cfg.enable (let
|
||||
command = "${getExe cfg.package} -c ${runtimeConfigPath}";
|
||||
in {
|
||||
systemd.services.copyparty = {
|
||||
description = "http file sharing hub";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
wantedBy = ["multi-user.target"];
|
||||
|
||||
environment = {
|
||||
PYTHONUNBUFFERED = "true";
|
||||
XDG_CONFIG_HOME = "${home}/.config";
|
||||
XDG_CONFIG_HOME = externalStateDir;
|
||||
};
|
||||
|
||||
preStart = let
|
||||
replaceSecretCommand = name: attrs:
|
||||
"${getExe pkgs.replace-secret} '${
|
||||
passwordPlaceholder name
|
||||
}' '${attrs.passwordFile}' ${runtimeConfigPath}";
|
||||
replaceSecretCommand = name: attrs: "${getExe pkgs.replace-secret} '${
|
||||
passwordPlaceholder name
|
||||
}' '${attrs.passwordFile}' ${runtimeConfigPath}";
|
||||
in ''
|
||||
set -euo pipefail
|
||||
install -m 600 ${configFile} ${runtimeConfigPath}
|
||||
${concatStringsSep "\n"
|
||||
(mapAttrsToList replaceSecretCommand cfg.accounts)}
|
||||
(mapAttrsToList replaceSecretCommand cfg.accounts)}
|
||||
'';
|
||||
|
||||
serviceConfig = {
|
||||
Type = "simple";
|
||||
ExecStart = "${getExe cfg.package} -c ${runtimeConfigPath}";
|
||||
|
||||
ExecStart = command;
|
||||
# Hardening options
|
||||
User = "copyparty";
|
||||
Group = "copyparty";
|
||||
RuntimeDirectory = name;
|
||||
User = cfg.user;
|
||||
Group = cfg.group;
|
||||
RuntimeDirectory = ["copyparty"];
|
||||
RuntimeDirectoryMode = "0700";
|
||||
StateDirectory = [ name "${name}/data" "${name}/.config" ];
|
||||
StateDirectory = ["copyparty"];
|
||||
StateDirectoryMode = "0700";
|
||||
WorkingDirectory = home;
|
||||
CacheDirectory = lib.mkIf (cfg.settings ? hist) ["copyparty"];
|
||||
CacheDirectoryMode = lib.mkIf (cfg.settings ? hist) "0700";
|
||||
WorkingDirectory = externalStateDir;
|
||||
BindReadOnlyPaths =
|
||||
[
|
||||
"/nix/store"
|
||||
"-/etc/resolv.conf"
|
||||
"-/etc/nsswitch.conf"
|
||||
"-/etc/hosts"
|
||||
"-/etc/localtime"
|
||||
]
|
||||
++ (mapAttrsToList (k: v: "-${v.passwordFile}") cfg.accounts);
|
||||
BindPaths =
|
||||
(
|
||||
if cfg.settings ? hist
|
||||
then [cfg.settings.hist]
|
||||
else []
|
||||
)
|
||||
++ [externalStateDir]
|
||||
++ (mapAttrsToList (k: v: v.path) cfg.volumes);
|
||||
# ProtectSystem = "strict";
|
||||
# Note that unlike what 'ro' implies,
|
||||
# this actually makes it impossible to read anything in the root FS,
|
||||
# except for things explicitly mounted via `RuntimeDirectory`, `StateDirectory`, `CacheDirectory`, and `BindReadOnlyPaths`.
|
||||
# This is because TemporaryFileSystem creates a *new* *empty* filesystem for the process, so only bindmounts are visible.
|
||||
TemporaryFileSystem = "/:ro";
|
||||
BindReadOnlyPaths = [
|
||||
"/nix/store"
|
||||
"-/etc/resolv.conf"
|
||||
"-/etc/nsswitch.conf"
|
||||
"-/etc/hosts"
|
||||
"-/etc/localtime"
|
||||
] ++ (mapAttrsToList (k: v: "-${v.passwordFile}") cfg.accounts);
|
||||
BindPaths = [ home ] ++ (mapAttrsToList (k: v: v.path) cfg.volumes);
|
||||
# Would re-mount paths ignored by temporary root
|
||||
#ProtectSystem = "strict";
|
||||
ProtectHome = true;
|
||||
PrivateTmp = true;
|
||||
PrivateDevices = true;
|
||||
ProtectKernelTunables = true;
|
||||
@@ -269,15 +316,48 @@ in {
|
||||
NoNewPrivileges = true;
|
||||
LockPersonality = true;
|
||||
RestrictRealtime = true;
|
||||
MemoryDenyWriteExecute = true;
|
||||
};
|
||||
};
|
||||
|
||||
users.groups.copyparty = { };
|
||||
users.users.copyparty = {
|
||||
# ensure volumes exist:
|
||||
systemd.tmpfiles.settings."copyparty" = (
|
||||
lib.attrsets.mapAttrs' (
|
||||
name: value:
|
||||
lib.attrsets.nameValuePair (value.path) {
|
||||
d = {
|
||||
#: in front of things means it wont change it if the directory already exists.
|
||||
group = ":${cfg.group}";
|
||||
user = ":${cfg.user}";
|
||||
mode = ":755";
|
||||
};
|
||||
}
|
||||
)
|
||||
cfg.volumes
|
||||
);
|
||||
|
||||
users.groups.copyparty = lib.mkIf (cfg.user == "copyparty" && cfg.group == "copyparty") {};
|
||||
users.users.copyparty = lib.mkIf (cfg.user == "copyparty" && cfg.group == "copyparty") {
|
||||
description = "Service user for copyparty";
|
||||
group = "copyparty";
|
||||
home = home;
|
||||
home = externalStateDir;
|
||||
isSystemUser = true;
|
||||
};
|
||||
};
|
||||
environment.systemPackages = lib.mkIf cfg.mkHashWrapper [
|
||||
(pkgs.writeShellScriptBin
|
||||
"copyparty-hash"
|
||||
''
|
||||
set -a # automatically export variables
|
||||
# set same environment variables as the systemd service
|
||||
${lib.pipe config.systemd.services.copyparty.environment [
|
||||
(lib.filterAttrs (n: v: v != null && n != "PATH"))
|
||||
(lib.mapAttrs (_: v: "${v}"))
|
||||
(lib.toShellVars)
|
||||
]}
|
||||
PATH=${config.systemd.services.copyparty.environment.PATH}:$PATH
|
||||
|
||||
exec ${command} --ah-cli
|
||||
'')
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Maintainer: icxes <dev.null@need.moe>
|
||||
pkgname=copyparty
|
||||
pkgver="1.16.21"
|
||||
pkgver="1.17.0"
|
||||
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=("2e416e18dc854c65643b8aaedca56e0a5c5a03b0c3d45b7ff3f68daa38d8e9c6")
|
||||
sha256sums=("d8a49b3398f4cdb0754dd8b9e3ab32544e44e2fee94c88903f65ffc003b6eeec")
|
||||
|
||||
build() {
|
||||
cd "${srcdir}/${pkgname}-${pkgver}"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"url": "https://github.com/9001/copyparty/releases/download/v1.16.21/copyparty-sfx.py",
|
||||
"version": "1.16.21",
|
||||
"hash": "sha256-+/f4g8J2Mv0l6ChXzbNJ84G8LeB+mP1UfkWzQxizd/g="
|
||||
"url": "https://github.com/9001/copyparty/releases/download/v1.17.0/copyparty-sfx.py",
|
||||
"version": "1.17.0",
|
||||
"hash": "sha256-iRqaXQvwX4DceOhZmI6g9KXeR+rFAWnNHK/GTHkoQ7Q="
|
||||
}
|
||||
@@ -1003,6 +1003,9 @@ def add_upload(ap):
|
||||
ap2 = ap.add_argument_group('upload options')
|
||||
ap2.add_argument("--dotpart", action="store_true", help="dotfile incomplete uploads, hiding them from clients unless \033[33m-ed\033[0m")
|
||||
ap2.add_argument("--plain-ip", action="store_true", help="when avoiding filename collisions by appending the uploader's ip to the filename: append the plaintext ip instead of salting and hashing the ip")
|
||||
ap2.add_argument("--put-name", metavar="TXT", type=u, default="put-{now.6f}-{cip}.bin", help="filename for nameless uploads (when uploader doesn't provide a name); default is [\033[32mput-UNIXTIME-IP.bin\033[0m] (the \033[32m.6f\033[0m means six decimal places) (volflag=put_name)")
|
||||
ap2.add_argument("--put-ck", metavar="ALG", type=u, default="sha512", help="default checksum-hasher for PUT/WebDAV uploads: no / md5 / sha1 / sha256 / sha512 / b2 / blake2 / b2s / blake2s (volflag=put_ck)")
|
||||
ap2.add_argument("--bup-ck", metavar="ALG", type=u, default="sha512", help="default checksum-hasher for bup/basic-uploader: no / md5 / sha1 / sha256 / sha512 / b2 / blake2 / b2s / blake2s (volflag=bup_ck)")
|
||||
ap2.add_argument("--unpost", metavar="SEC", type=int, default=3600*12, help="grace period where uploads can be deleted by the uploader, even without delete permissions; 0=disabled, default=12h")
|
||||
ap2.add_argument("--u2abort", metavar="NUM", type=int, default=1, help="clients can abort incomplete uploads by using the unpost tab (requires \033[33m-e2d\033[0m). [\033[32m0\033[0m] = never allowed (disable feature), [\033[32m1\033[0m] = allow if client has the same IP as the upload AND is using the same account, [\033[32m2\033[0m] = just check the IP, [\033[32m3\033[0m] = just check account-name (volflag=u2abort)")
|
||||
ap2.add_argument("--blank-wt", metavar="SEC", type=int, default=300, help="file write grace period (any client can write to a blank file last-modified more recently than \033[33mSEC\033[0m seconds ago)")
|
||||
@@ -1379,8 +1382,8 @@ def add_thumbnail(ap):
|
||||
ap2.add_argument("--th-r-vips", metavar="T,T", type=u, default="avif,exr,fit,fits,fts,gif,hdr,heic,jp2,jpeg,jpg,jpx,jxl,nii,pfm,pgm,png,ppm,svg,tif,tiff,webp", help="image formats to decode using pyvips")
|
||||
ap2.add_argument("--th-r-ffi", metavar="T,T", type=u, default="apng,avif,avifs,bmp,cbz,dds,dib,fit,fits,fts,gif,hdr,heic,heics,heif,heifs,icns,ico,jp2,jpeg,jpg,jpx,jxl,pbm,pcx,pfm,pgm,png,pnm,ppm,psd,qoi,sgi,tga,tif,tiff,webp,xbm,xpm", help="image formats to decode using ffmpeg")
|
||||
ap2.add_argument("--th-r-ffv", metavar="T,T", type=u, default="3gp,asf,av1,avc,avi,flv,h264,h265,hevc,m4v,mjpeg,mjpg,mkv,mov,mp4,mpeg,mpeg2,mpegts,mpg,mpg2,mts,nut,ogm,ogv,rm,ts,vob,webm,wmv", help="video formats to decode using ffmpeg")
|
||||
ap2.add_argument("--th-r-ffa", metavar="T,T", type=u, default="aac,ac3,aif,aiff,alac,alaw,amr,apac,ape,au,bonk,dfpwm,dts,flac,gsm,ilbc,it,itgz,itxz,itz,m4a,mdgz,mdxz,mdz,mo3,mod,mp2,mp3,mpc,mptm,mt2,mulaw,ogg,okt,opus,ra,s3m,s3gz,s3xz,s3z,tak,tta,ulaw,wav,wma,wv,xm,xmgz,xmxz,xmz,xpk", help="audio formats to decode using ffmpeg")
|
||||
ap2.add_argument("--th-spec-cnv", metavar="T,T", type=u, default="it,itgz,itxz,itz,mdgz,mdxz,mdz,mo3,mod,s3m,s3gz,s3xz,s3z,xm,xmgz,xmxz,xmz,xpk", help="audio formats which provoke https://trac.ffmpeg.org/ticket/10797 (huge ram usage for s3xmodit spectrograms)")
|
||||
ap2.add_argument("--th-r-ffa", metavar="T,T", type=u, default="aac,ac3,aif,aiff,alac,alaw,amr,apac,ape,au,bonk,dfpwm,dts,flac,gsm,ilbc,it,itgz,itxz,itz,m4a,mdgz,mdxz,mdz,mo3,mod,mp2,mp3,mpc,mptm,mt2,mulaw,oga,ogg,okt,opus,ra,s3m,s3gz,s3xz,s3z,tak,tta,ulaw,wav,wma,wv,xm,xmgz,xmxz,xmz,xpk", help="audio formats to decode using ffmpeg")
|
||||
ap2.add_argument("--th-spec-cnv", metavar="T", type=u, default="it,itgz,itxz,itz,mdgz,mdxz,mdz,mo3,mod,s3m,s3gz,s3xz,s3z,xm,xmgz,xmxz,xmz,xpk", help="audio formats which provoke https://trac.ffmpeg.org/ticket/10797 (huge ram usage for s3xmodit spectrograms)")
|
||||
ap2.add_argument("--au-unpk", metavar="E=F.C", type=u, default="mdz=mod.zip, mdgz=mod.gz, mdxz=mod.xz, s3z=s3m.zip, s3gz=s3m.gz, s3xz=s3m.xz, xmz=xm.zip, xmgz=xm.gz, xmxz=xm.xz, itz=it.zip, itgz=it.gz, itxz=it.xz, cbz=jpg.cbz", help="audio/image formats to decompress before passing to ffmpeg")
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# coding: utf-8
|
||||
|
||||
VERSION = (1, 17, 0)
|
||||
VERSION = (1, 17, 1)
|
||||
CODENAME = "mixtape.m3u"
|
||||
BUILD_DT = (2025, 4, 26)
|
||||
BUILD_DT = (2025, 5, 18)
|
||||
|
||||
S_VERSION = ".".join(map(str, VERSION))
|
||||
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)
|
||||
|
||||
@@ -2074,6 +2074,10 @@ class AuthSrv(object):
|
||||
if len(zs) == 3: # fc5 => ffcc55
|
||||
vol.flags["tcolor"] = "".join([x * 2 for x in zs])
|
||||
|
||||
# volflag syntax currently doesn't allow for ':' in value
|
||||
zs = vol.flags["put_name"]
|
||||
vol.flags["put_name2"] = zs.replace("{now.", "{now:.")
|
||||
|
||||
if vol.flags.get("neversymlink"):
|
||||
vol.flags["hardlinkonly"] = True # was renamed
|
||||
if vol.flags.get("hardlinkonly"):
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import calendar
|
||||
import errno
|
||||
import filecmp
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import time
|
||||
|
||||
from .__init__ import ANYWIN
|
||||
from .util import Netdev, load_resource, runcmd, wrename, wunlink
|
||||
from .util import Netdev, atomic_move, load_resource, runcmd, wunlink
|
||||
|
||||
HAVE_CFSSL = not os.environ.get("PRTY_NO_CFSSL")
|
||||
|
||||
@@ -122,7 +120,7 @@ def _gen_ca(log: "RootLogger", args):
|
||||
wunlink(nlog, bname + ".key", VF)
|
||||
except:
|
||||
pass
|
||||
wrename(nlog, bname + "-key.pem", bname + ".key", VF)
|
||||
atomic_move(nlog, bname + "-key.pem", bname + ".key", VF)
|
||||
wunlink(nlog, bname + ".csr", VF)
|
||||
|
||||
log("cert", "new ca OK", 2)
|
||||
@@ -215,7 +213,7 @@ def _gen_srv(log: "RootLogger", args, netdevs: dict[str, Netdev]):
|
||||
wunlink(nlog, bname + ".key", VF)
|
||||
except:
|
||||
pass
|
||||
wrename(nlog, bname + "-key.pem", bname + ".key", VF)
|
||||
atomic_move(nlog, bname + "-key.pem", bname + ".key", VF)
|
||||
wunlink(nlog, bname + ".csr", VF)
|
||||
|
||||
with open(os.path.join(args.crt_dir, "ca.pem"), "rb") as f:
|
||||
|
||||
@@ -75,6 +75,7 @@ def vf_vmap() -> dict[str, str]:
|
||||
"th_x3": "th3x",
|
||||
}
|
||||
for k in (
|
||||
"bup_ck",
|
||||
"dbd",
|
||||
"forget_ip",
|
||||
"hsortn",
|
||||
@@ -95,6 +96,8 @@ def vf_vmap() -> dict[str, str]:
|
||||
"og_title_i",
|
||||
"og_tpl",
|
||||
"og_ua",
|
||||
"put_ck",
|
||||
"put_name",
|
||||
"mv_retry",
|
||||
"rm_retry",
|
||||
"sort",
|
||||
@@ -165,6 +168,9 @@ flagcats = {
|
||||
"daw": "enable full WebDAV write support (dangerous);\nPUT-operations will now \033[1;31mOVERWRITE\033[0;35m existing files",
|
||||
"nosub": "forces all uploads into the top folder of the vfs",
|
||||
"magic": "enables filetype detection for nameless uploads",
|
||||
"put_name": "fallback filename for nameless uploads",
|
||||
"put_ck": "default checksum-hasher for PUT/WebDAV uploads",
|
||||
"bup_ck": "default checksum-hasher for bup/basic uploads",
|
||||
"gz": "allows server-side gzip compression of uploads with ?gz",
|
||||
"xz": "allows server-side lzma compression of uploads with ?xz",
|
||||
"pk": "forces server-side compression, optional arg: xz,9",
|
||||
|
||||
@@ -113,7 +113,6 @@ from .util import (
|
||||
vol_san,
|
||||
vroots,
|
||||
vsplit,
|
||||
wrename,
|
||||
wunlink,
|
||||
yieldfile,
|
||||
)
|
||||
@@ -1413,7 +1412,7 @@ class HttpCli(object):
|
||||
desc = "%s - %s" % (tag_a, tag_t) if tag_t and tag_a else (tag_t or tag_a)
|
||||
desc = html_escape(desc, True, True) if desc else title
|
||||
mime = html_escape(guess_mime(title))
|
||||
lmod = formatdate(i["ts"])
|
||||
lmod = formatdate(max(0, i["ts"]))
|
||||
zsa = (iurl, iurl, title, desc, lmod, iurl, mime, i["sz"])
|
||||
zs = (
|
||||
"""\
|
||||
@@ -1570,7 +1569,7 @@ class HttpCli(object):
|
||||
for x in fgen:
|
||||
rp = vjoin(vtop, x["vp"])
|
||||
st: os.stat_result = x["st"]
|
||||
mtime = st.st_mtime
|
||||
mtime = max(0, st.st_mtime)
|
||||
if stat.S_ISLNK(st.st_mode):
|
||||
try:
|
||||
st = bos.stat(os.path.join(tap, x["vp"]))
|
||||
@@ -2100,8 +2099,7 @@ class HttpCli(object):
|
||||
suffix = "-{:.6f}-{}".format(time.time(), self.dip())
|
||||
nameless = not fn
|
||||
if nameless:
|
||||
suffix += ".bin"
|
||||
fn = "put" + suffix
|
||||
fn = vfs.flags["put_name2"].format(now=time.time(), cip=self.dip())
|
||||
|
||||
params = {"suffix": suffix, "fdir": fdir}
|
||||
if self.args.nw:
|
||||
@@ -2181,28 +2179,26 @@ class HttpCli(object):
|
||||
# small toctou, but better than clobbering a hardlink
|
||||
wunlink(self.log, path, vfs.flags)
|
||||
|
||||
halg = "sha512"
|
||||
hasher = None
|
||||
copier = hashcopy
|
||||
if "ck" in self.ouparam or "ck" in self.headers:
|
||||
halg = zs = self.ouparam.get("ck") or self.headers.get("ck") or ""
|
||||
if not zs or zs == "no":
|
||||
copier = justcopy
|
||||
halg = ""
|
||||
elif zs == "md5":
|
||||
hasher = hashlib.md5(**USED4SEC)
|
||||
elif zs == "sha1":
|
||||
hasher = hashlib.sha1(**USED4SEC)
|
||||
elif zs == "sha256":
|
||||
hasher = hashlib.sha256(**USED4SEC)
|
||||
elif zs in ("blake2", "b2"):
|
||||
hasher = hashlib.blake2b(**USED4SEC)
|
||||
elif zs in ("blake2s", "b2s"):
|
||||
hasher = hashlib.blake2s(**USED4SEC)
|
||||
elif zs == "sha512":
|
||||
pass
|
||||
else:
|
||||
raise Pebkac(500, "unknown hash alg")
|
||||
halg = self.ouparam.get("ck") or self.headers.get("ck") or vfs.flags["put_ck"]
|
||||
if halg == "sha512":
|
||||
pass
|
||||
elif halg == "no":
|
||||
copier = justcopy
|
||||
halg = ""
|
||||
elif halg == "md5":
|
||||
hasher = hashlib.md5(**USED4SEC)
|
||||
elif halg == "sha1":
|
||||
hasher = hashlib.sha1(**USED4SEC)
|
||||
elif halg == "sha256":
|
||||
hasher = hashlib.sha256(**USED4SEC)
|
||||
elif halg in ("blake2", "b2"):
|
||||
hasher = hashlib.blake2b(**USED4SEC)
|
||||
elif halg in ("blake2s", "b2s"):
|
||||
hasher = hashlib.blake2s(**USED4SEC)
|
||||
else:
|
||||
raise Pebkac(500, "unknown hash alg")
|
||||
|
||||
f, fn = ren_open(fn, *open_a, **params)
|
||||
try:
|
||||
@@ -2931,7 +2927,8 @@ class HttpCli(object):
|
||||
self.parser.drop()
|
||||
|
||||
self.log("logout " + self.uname)
|
||||
self.asrv.forget_session(self.conn.hsrv.broker, self.uname)
|
||||
if not self.uname.startswith("s_"):
|
||||
self.asrv.forget_session(self.conn.hsrv.broker, self.uname)
|
||||
self.get_pwd_cookie("x")
|
||||
|
||||
dst = self.args.SRS + "?h"
|
||||
@@ -3084,15 +3081,18 @@ class HttpCli(object):
|
||||
vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True)
|
||||
self._assert_safe_rem(rem)
|
||||
|
||||
halg = "sha512"
|
||||
hasher = None
|
||||
copier = hashcopy
|
||||
if nohash:
|
||||
halg = ""
|
||||
copier = justcopy
|
||||
elif "ck" in self.ouparam or "ck" in self.headers:
|
||||
halg = self.ouparam.get("ck") or self.headers.get("ck") or ""
|
||||
if not halg or halg == "no":
|
||||
else:
|
||||
copier = hashcopy
|
||||
halg = (
|
||||
self.ouparam.get("ck") or self.headers.get("ck") or vfs.flags["bup_ck"]
|
||||
)
|
||||
if halg == "sha512":
|
||||
pass
|
||||
elif halg == "no":
|
||||
copier = justcopy
|
||||
halg = ""
|
||||
elif halg == "md5":
|
||||
@@ -3105,8 +3105,6 @@ class HttpCli(object):
|
||||
hasher = hashlib.blake2b(**USED4SEC)
|
||||
elif halg in ("blake2s", "b2s"):
|
||||
hasher = hashlib.blake2s(**USED4SEC)
|
||||
elif halg == "sha512":
|
||||
pass
|
||||
else:
|
||||
raise Pebkac(500, "unknown hash alg")
|
||||
|
||||
@@ -3569,7 +3567,7 @@ class HttpCli(object):
|
||||
except:
|
||||
pass
|
||||
if dp:
|
||||
wrename(self.log, fp, os.path.join(dp, mfile2), vfs.flags)
|
||||
atomic_move(self.log, fp, os.path.join(dp, mfile2), vfs.flags)
|
||||
|
||||
assert self.parser.gen # !rm
|
||||
p_field, _, p_data = next(self.parser.gen)
|
||||
@@ -3980,7 +3978,7 @@ class HttpCli(object):
|
||||
if ptop is not None:
|
||||
assert job and ap_data # type: ignore # !rm
|
||||
sz = job["size"]
|
||||
file_ts = job["lmod"]
|
||||
file_ts = max(0, job["lmod"])
|
||||
editions["plain"] = (ap_data, sz)
|
||||
break
|
||||
|
||||
@@ -5504,6 +5502,7 @@ class HttpCli(object):
|
||||
raise Pebkac(400, "selected file not found on disk: [%s]" % (fn,))
|
||||
|
||||
pw = req.get("pw") or ""
|
||||
pw = self.asrv.ah.hash(pw)
|
||||
now = int(time.time())
|
||||
sexp = req["exp"]
|
||||
exp = int(sexp) if sexp else 0
|
||||
@@ -6121,7 +6120,7 @@ class HttpCli(object):
|
||||
margin = "-"
|
||||
|
||||
sz = inf.st_size
|
||||
zd = datetime.fromtimestamp(linf.st_mtime, UTC)
|
||||
zd = datetime.fromtimestamp(max(0, linf.st_mtime), UTC)
|
||||
dt = "%04d-%02d-%02d %02d:%02d:%02d" % (
|
||||
zd.year,
|
||||
zd.month,
|
||||
|
||||
@@ -17,6 +17,9 @@ if True: # pylint: disable=using-constant-test
|
||||
from .util import NamedLogger
|
||||
|
||||
|
||||
TAR_NO_OPUS = set("aac|m4a|mp3|oga|ogg|opus|wma".split("|"))
|
||||
|
||||
|
||||
class StreamArc(object):
|
||||
def __init__(
|
||||
self,
|
||||
@@ -82,9 +85,7 @@ def enthumb(
|
||||
) -> dict[str, Any]:
|
||||
rem = f["vp"]
|
||||
ext = rem.rsplit(".", 1)[-1].lower()
|
||||
if (fmt == "mp3" and ext == "mp3") or (
|
||||
fmt == "opus" and ext in "aac|m4a|mp3|ogg|opus|wma".split("|")
|
||||
):
|
||||
if (fmt == "mp3" and ext == "mp3") or (fmt == "opus" and ext in TAR_NO_OPUS):
|
||||
raise Exception()
|
||||
|
||||
vp = vjoin(vtop, rem.split("/", 1)[1])
|
||||
|
||||
@@ -284,6 +284,7 @@ class Tftpd(object):
|
||||
if not ptn or not ptn.match(fn.lower()):
|
||||
return None
|
||||
|
||||
tsdt = datetime.fromtimestamp
|
||||
vn, rem = self.asrv.vfs.get(vpath, "*", True, False)
|
||||
fsroot, vfs_ls, vfs_virt = vn.ls(
|
||||
rem,
|
||||
@@ -296,7 +297,7 @@ class Tftpd(object):
|
||||
dirs1 = [(v.st_mtime, v.st_size, k + "/") for k, v in vfs_ls if k in dnames]
|
||||
fils1 = [(v.st_mtime, v.st_size, k) for k, v in vfs_ls if k not in dnames]
|
||||
real1 = dirs1 + fils1
|
||||
realt = [(datetime.fromtimestamp(mt, UTC), sz, fn) for mt, sz, fn in real1]
|
||||
realt = [(tsdt(max(0, mt), UTC), sz, fn) for mt, sz, fn in real1]
|
||||
reals = [
|
||||
(
|
||||
"%04d-%02d-%02d %02d:%02d:%02d"
|
||||
|
||||
@@ -24,13 +24,13 @@ from .util import (
|
||||
Cooldown,
|
||||
Daemon,
|
||||
afsenc,
|
||||
atomic_move,
|
||||
fsenc,
|
||||
min_ex,
|
||||
runcmd,
|
||||
statdir,
|
||||
ub64enc,
|
||||
vsplit,
|
||||
wrename,
|
||||
wunlink,
|
||||
)
|
||||
|
||||
@@ -412,7 +412,7 @@ class ThumbSrv(object):
|
||||
wunlink(self.log, ap_unpk, vn.flags)
|
||||
|
||||
try:
|
||||
wrename(self.log, ttpath, tpath, vn.flags)
|
||||
atomic_move(self.log, ttpath, tpath, vn.flags)
|
||||
except Exception as ex:
|
||||
if not os.path.exists(tpath):
|
||||
t = "failed to move [%s] to [%s]: %r"
|
||||
@@ -677,7 +677,7 @@ class ThumbSrv(object):
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
wrename(self.log, wtpath, tpath, vn.flags)
|
||||
atomic_move(self.log, wtpath, tpath, vn.flags)
|
||||
|
||||
def conv_spec(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
|
||||
ret, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
|
||||
|
||||
@@ -1119,7 +1119,7 @@ class Up2k(object):
|
||||
ft = "\033[0;32m{}{:.0}"
|
||||
ff = "\033[0;35m{}{:.0}"
|
||||
fv = "\033[0;36m{}:\033[90m{}"
|
||||
zs = "ext_th_d html_head mv_re_r mv_re_t rm_re_r rm_re_t srch_re_dots srch_re_nodot zipmax zipmaxn_v zipmaxs_v"
|
||||
zs = "ext_th_d html_head put_name2 mv_re_r mv_re_t rm_re_r rm_re_t srch_re_dots srch_re_nodot zipmax zipmaxn_v zipmaxs_v"
|
||||
fx = set(zs.split())
|
||||
fd = vf_bmap()
|
||||
fd.update(vf_cmap())
|
||||
@@ -2120,11 +2120,12 @@ class Up2k(object):
|
||||
return -1
|
||||
|
||||
w = bw[:-1].decode("ascii")
|
||||
w16 = w[:16]
|
||||
|
||||
with self.mutex:
|
||||
try:
|
||||
q = "select rd, fn, ip, at from up where substr(w,1,16)=? and +w=?"
|
||||
rd, fn, ip, at = cur.execute(q, (w[:16], w)).fetchone()
|
||||
rd, fn, ip, at = cur.execute(q, (w16, w)).fetchone()
|
||||
except:
|
||||
# file modified/deleted since spooling
|
||||
continue
|
||||
@@ -2133,8 +2134,12 @@ class Up2k(object):
|
||||
rd, fn = s3dec(rd, fn)
|
||||
|
||||
if "mtp" in flags:
|
||||
q = "select 1 from mt where w=? and +k='t:mtp' limit 1"
|
||||
if cur.execute(q, (w16,)).fetchone():
|
||||
continue
|
||||
|
||||
q = "insert into mt values (?,'t:mtp','a')"
|
||||
cur.execute(q, (w[:16],))
|
||||
cur.execute(q, (w16,))
|
||||
|
||||
abspath = djoin(ptop, rd, fn)
|
||||
self.pp.msg = "c%d %s" % (nq, abspath)
|
||||
@@ -2190,7 +2195,7 @@ class Up2k(object):
|
||||
return tf, -1
|
||||
|
||||
if flt == 1:
|
||||
q = "select w from mt where w = ?"
|
||||
q = "select 1 from mt where w=? and +k != 't:mtp'"
|
||||
if c2.execute(q, (row[0][:16],)).fetchone():
|
||||
continue
|
||||
|
||||
@@ -3231,7 +3236,7 @@ class Up2k(object):
|
||||
if hr.get("reloc"):
|
||||
x = pathmod(self.vfs, dst, vp, hr["reloc"])
|
||||
if x:
|
||||
zvfs = vfs
|
||||
ud1 = (vfs.vpath, job["prel"], job["name"])
|
||||
pdir, _, job["name"], (vfs, rem) = x
|
||||
dst = os.path.join(pdir, job["name"])
|
||||
job["vcfg"] = vfs.flags
|
||||
@@ -3239,7 +3244,8 @@ class Up2k(object):
|
||||
job["vtop"] = vfs.vpath
|
||||
job["prel"] = rem
|
||||
job["name"] = sanitize_fn(job["name"], "")
|
||||
if zvfs.vpath != vfs.vpath:
|
||||
ud2 = (vfs.vpath, job["prel"], job["name"])
|
||||
if ud1 != ud2:
|
||||
# print(json.dumps(job, sort_keys=True, indent=4))
|
||||
job["hash"] = cj["hash"]
|
||||
self.log("xbu reloc1:%d..." % (depth,), 6)
|
||||
@@ -4994,14 +5000,15 @@ class Up2k(object):
|
||||
if hr.get("reloc"):
|
||||
x = pathmod(self.vfs, ap_chk, vp_chk, hr["reloc"])
|
||||
if x:
|
||||
zvfs = vfs
|
||||
ud1 = (vfs.vpath, job["prel"], job["name"])
|
||||
pdir, _, job["name"], (vfs, rem) = x
|
||||
job["vcfg"] = vf = vfs.flags
|
||||
job["ptop"] = vfs.realpath
|
||||
job["vtop"] = vfs.vpath
|
||||
job["prel"] = rem
|
||||
job["name"] = sanitize_fn(job["name"], "")
|
||||
if zvfs.vpath != vfs.vpath:
|
||||
ud2 = (vfs.vpath, job["prel"], job["name"])
|
||||
if ud1 != ud2:
|
||||
self.log("xbu reloc2:%d..." % (depth,), 6)
|
||||
return self._handle_json(job, depth + 1)
|
||||
|
||||
|
||||
@@ -2583,6 +2583,11 @@ def _fs_mvrm(
|
||||
now = time.time()
|
||||
if ex.errno == errno.ENOENT:
|
||||
return False
|
||||
if not attempt and ex.errno == errno.EXDEV:
|
||||
t = "using copy+delete (%s)\n %s\n %s"
|
||||
log(t % (ex.strerror, src, dst))
|
||||
osfun = shutil.move
|
||||
continue
|
||||
if now - t0 > maxtime or attempt == 90209:
|
||||
raise
|
||||
if not attempt:
|
||||
@@ -2607,15 +2612,18 @@ def atomic_move(log: "NamedLogger", src: str, dst: str, flags: dict[str, Any]) -
|
||||
elif flags.get("mv_re_t"):
|
||||
_fs_mvrm(log, src, dst, True, flags)
|
||||
else:
|
||||
os.replace(bsrc, bdst)
|
||||
|
||||
|
||||
def wrename(log: "NamedLogger", src: str, dst: str, flags: dict[str, Any]) -> bool:
|
||||
if not flags.get("mv_re_t"):
|
||||
os.rename(fsenc(src), fsenc(dst))
|
||||
return True
|
||||
|
||||
return _fs_mvrm(log, src, dst, False, flags)
|
||||
try:
|
||||
os.replace(bsrc, bdst)
|
||||
except OSError as ex:
|
||||
if ex.errno != errno.EXDEV:
|
||||
raise
|
||||
t = "using copy+delete (%s);\n %s\n %s"
|
||||
log(t % (ex.strerror, src, dst))
|
||||
try:
|
||||
os.unlink(bdst)
|
||||
except:
|
||||
pass
|
||||
shutil.move(bsrc, bdst)
|
||||
|
||||
|
||||
def wunlink(log: "NamedLogger", abspath: str, flags: dict[str, Any]) -> bool:
|
||||
|
||||
@@ -1160,8 +1160,8 @@ html.y #widget.open {
|
||||
border: 1px solid var(--bg-u5);
|
||||
border-width: 0 .1em 0 0;
|
||||
}
|
||||
#wfm.act+#wzip,
|
||||
#wfm.act+#wzip+#wnp {
|
||||
#wfm.act+#wzip1+#wzip,
|
||||
#wfm.act+#wzip1+#wzip+#wnp {
|
||||
margin-left: .2em;
|
||||
padding-left: .2em;
|
||||
border-left-width: .1em;
|
||||
@@ -1179,12 +1179,14 @@ html.y #widget.open {
|
||||
#wtoggle.np #wnp {
|
||||
display: inline-block;
|
||||
}
|
||||
#wtoggle.sel #wzip1,
|
||||
#wtoggle.sel.np #wnp {
|
||||
display: none;
|
||||
}
|
||||
#wfm a,
|
||||
#wnp a,
|
||||
#wm3u a,
|
||||
#zip1,
|
||||
#wzip a {
|
||||
font-size: .5em;
|
||||
padding: 0 .3em;
|
||||
@@ -1192,6 +1194,9 @@ html.y #widget.open {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
#zip1 {
|
||||
font-size: .38em;
|
||||
}
|
||||
#wm3u a {
|
||||
margin: -.2em .1em;
|
||||
font-size: .45em;
|
||||
@@ -1205,10 +1210,14 @@ html.y #widget.open {
|
||||
}
|
||||
#wfm span,
|
||||
#wm3u span,
|
||||
#zip1 span,
|
||||
#wnp span {
|
||||
font-size: .6em;
|
||||
display: block;
|
||||
}
|
||||
#zip1 span {
|
||||
font-size: .9em;
|
||||
}
|
||||
#wnp span {
|
||||
font-size: .7em;
|
||||
}
|
||||
|
||||
@@ -140,6 +140,7 @@ var Ls = {
|
||||
"wt_pst": "paste a previously cut / copied selection$NHotkey: ctrl-V",
|
||||
"wt_selall": "select all files$NHotkey: ctrl-A (when file focused)",
|
||||
"wt_selinv": "invert selection",
|
||||
"wt_zip1": "download this folder as archive",
|
||||
"wt_selzip": "download selection as archive",
|
||||
"wt_seldl": "download selection as separate files$NHotkey: Y",
|
||||
"wt_npirc": "copy irc-formatted track info",
|
||||
@@ -754,6 +755,7 @@ var Ls = {
|
||||
"wt_pst": "lim inn filer (som tidligere ble klippet ut / kopiert et annet sted)$NSnarvei: ctrl-V",
|
||||
"wt_selall": "velg alle filer$NSnarvei: ctrl-A (mens fokus er på en fil)",
|
||||
"wt_selinv": "inverter utvalg",
|
||||
"wt_zip1": "last ned denne mappen som et arkiv",
|
||||
"wt_selzip": "last ned de valgte filene som et arkiv",
|
||||
"wt_seldl": "last ned de valgte filene$NSnarvei: Y",
|
||||
"wt_npirc": "kopiér sang-info (irc-formatert)",
|
||||
@@ -1368,10 +1370,13 @@ var Ls = {
|
||||
"wt_pst": "粘贴之前剪切/复制的选择$N快捷键: ctrl-V",
|
||||
"wt_selall": "选择所有文件$N快捷键: ctrl-A(当文件被聚焦时)",
|
||||
"wt_selinv": "反转选择",
|
||||
"wt_zip1": "将此文件夹下载为归档文件", //m
|
||||
"wt_selzip": "将选择下载为归档文件",
|
||||
"wt_seldl": "将选择下载为单独的文件$N快捷键: Y",
|
||||
"wt_npirc": "复制 IRC 格式的曲目信息",
|
||||
"wt_nptxt": "复制纯文本格式的曲目信息",
|
||||
"wt_m3ua": "添加到 m3u 播放列表(稍后点击 <code>📻copy</code>)", //m
|
||||
"wt_m3uc": "复制 m3u 播放列表到剪贴板", //m
|
||||
"wt_grid": "切换网格/列表视图$N快捷键: G",
|
||||
"wt_prev": "上一曲$N快捷键: J",
|
||||
"wt_play": "播放/暂停$N快捷键: P",
|
||||
@@ -1508,6 +1513,7 @@ var Ls = {
|
||||
"mt_fau": "在手机上,如果下一首歌未能快速预加载,防止音乐停止(可能导致标签显示异常)\">☕️",
|
||||
"mt_waves": "波形进度条:$N显示音频幅度\">进度条",
|
||||
"mt_npclip": "显示当前播放歌曲的剪贴板按钮\">♪剪切板",
|
||||
"mt_m3u_c": "显示按钮以将所选歌曲$N复制为 m3u8 播放列表条目\">📻", //m
|
||||
"mt_octl": "操作系统集成(媒体快捷键 / OSD)\">OSD",
|
||||
"mt_oseek": "允许通过操作系统集成进行跳转$N$N注意:在某些设备(如 iPhone)上,$N这将替代下一首歌按钮\">seek",
|
||||
"mt_oscv": "在 OSD 中显示专辑封面\">封面",
|
||||
@@ -1533,6 +1539,7 @@ var Ls = {
|
||||
|
||||
"mb_play": "播放",
|
||||
"mm_hashplay": "播放这个音频文件?",
|
||||
"mm_m3u": "按 <code>Enter/确定</code> 播放\n按 <code>ESC/取消</code> 编辑", //m
|
||||
"mp_breq": "需要 Firefox 82+ 或 Chrome 73+ 或 iOS 15+",
|
||||
"mm_bload": "正在加载...",
|
||||
"mm_bconv": "正在转换为 {0},请稍等...",
|
||||
@@ -1560,6 +1567,7 @@ var Ls = {
|
||||
"f_bigtxt": "这个文件大小为 {0} MiB -- 真的以文本形式查看?",
|
||||
"fbd_more": '<div id="blazy">显示 <code>{0}</code> 个文件中的 <code>{1}</code> 个;<a href="#" id="bd_more">显示 {2}</a> 或 <a href="#" id="bd_all">显示全部</a></div>',
|
||||
"fbd_all": '<div id="blazy">显示 <code>{0}</code> 个文件中的 <code>{1}</code> 个;<a href="#" id="bd_all">显示全部</a></div>',
|
||||
"f_anota": "仅选择了 {0} 个项目,共 {1} 个;\n要选择整个文件夹,请先滚动到底部", //m
|
||||
|
||||
"f_dls": '当前文件夹中的文件链接已\n更改为下载链接',
|
||||
|
||||
@@ -1593,7 +1601,7 @@ var Ls = {
|
||||
"fs_tsrc": "共享的文件或文件夹",
|
||||
"fs_ppwd": "密码可选",
|
||||
"fs_w8": "正在创建文件共享...",
|
||||
"fs_ok": "按 <code>Enter/OK</code> 复制到剪贴板\n按 <code>ESC/Cancel</code> 关闭",
|
||||
"fs_ok": "按 <code>Enter/确定</code> 复制到剪贴板\n按 <code>ESC/取消</code> 关闭",
|
||||
|
||||
"frt_dec": "可能修复一些损坏的文件名\">url-decode",
|
||||
"frt_rst": "将修改后的文件名重置为原始文件名\">↺ 重置",
|
||||
@@ -1662,6 +1670,10 @@ var Ls = {
|
||||
"tvt_sel": "选择文件 (用于剪切/删除/...)$N快捷键: S\">选择",
|
||||
"tvt_edit": "在文本编辑器中打开文件$N快捷键: E\">✏️ 编辑",
|
||||
|
||||
"m3u_add1": "歌曲已添加到 m3u 播放列表", //m
|
||||
"m3u_addn": "已添加 {0} 首歌曲到 m3u 播放列表", //m
|
||||
"m3u_clip": "m3u 播放列表已复制到剪贴板\n\n请创建一个以 <code>.m3u</code> 结尾的文本文件,\n并将播放列表粘贴到该文件中;\n这样就可以播放了", //m
|
||||
|
||||
"gt_vau": "不显示视频,仅播放音频\">🎧",
|
||||
"gt_msel": "启用文件选择;按住 ctrl 键点击文件以覆盖$N$N<em>当启用时:双击文件/文件夹以打开它</em>$N$N快捷键:S\">多选",
|
||||
"gt_crop": "中心裁剪缩略图\">裁剪",
|
||||
@@ -1899,6 +1911,8 @@ ebi('widget').innerHTML = (
|
||||
' href="#" id="fcut" tt="' + L.wt_cut + '">✂<span>cut</span></a><a' +
|
||||
' href="#" id="fcpy" tt="' + L.wt_cpy + '">⧉<span>copy</span></a><a' +
|
||||
' href="#" id="fpst" tt="' + L.wt_pst + '">📋<span>paste</span></a>' +
|
||||
'</span><span id="wzip1"><a' +
|
||||
' href="#" id="zip1" tt="' + L.wt_zip1 + '">📦<span>zip</span></a>' +
|
||||
'</span><span id="wzip"><a' +
|
||||
' href="#" id="selall" tt="' + L.wt_selall + '">sel.<br />all</a><a' +
|
||||
' href="#" id="selinv" tt="' + L.wt_selinv + '">sel.<br />inv.</a><a' +
|
||||
@@ -2468,7 +2482,7 @@ var mpl = (function () {
|
||||
c = r.ac_flac;
|
||||
else if (/\.(aac|m4a)$/i.exec(cs))
|
||||
c = r.ac_aac;
|
||||
else if (/\.(ogg|opus)$/i.exec(cs) && (!can_ogg || mpl.ac2 == 'mp3'))
|
||||
else if (/\.(oga|ogg|opus)$/i.exec(cs) && (!can_ogg || mpl.ac2 == 'mp3'))
|
||||
c = true;
|
||||
else if (re_au_native.exec(cs))
|
||||
c = false;
|
||||
@@ -2655,8 +2669,8 @@ mpl.init_ac2();
|
||||
|
||||
|
||||
var re_m3u = /\.(m3u8?)$/i;
|
||||
var re_au_native = (can_ogg || have_acode) ? /\.(aac|flac|m4a|mp3|ogg|opus|wav)$/i : /\.(aac|flac|m4a|mp3|wav)$/i,
|
||||
re_au_all = /\.(aac|ac3|aif|aiff|alac|alaw|amr|ape|au|dfpwm|dts|flac|gsm|it|itgz|itxz|itz|m4a|mdgz|mdxz|mdz|mo3|mod|mp2|mp3|mpc|mptm|mt2|mulaw|ogg|okt|opus|ra|s3m|s3gz|s3xz|s3z|tak|tta|ulaw|wav|wma|wv|xm|xmgz|xmxz|xmz|xpk|3gp|asf|avi|flv|m4v|mkv|mov|mp4|mpeg|mpeg2|mpegts|mpg|mpg2|nut|ogm|ogv|rm|ts|vob|webm|wmv)$/i;
|
||||
var re_au_native = (can_ogg || have_acode) ? /\.(aac|flac|m4a|mp3|oga|ogg|opus|wav)$/i : /\.(aac|flac|m4a|mp3|wav)$/i,
|
||||
re_au_all = /\.(aac|ac3|aif|aiff|alac|alaw|amr|ape|au|dfpwm|dts|flac|gsm|it|itgz|itxz|itz|m4a|mdgz|mdxz|mdz|mo3|mod|mp2|mp3|mpc|mptm|mt2|mulaw|oga|ogg|okt|opus|ra|s3m|s3gz|s3xz|s3z|tak|tta|ulaw|wav|wma|wv|xm|xmgz|xmxz|xmz|xpk|3gp|asf|avi|flv|m4v|mkv|mov|mp4|mpeg|mpeg2|mpegts|mpg|mpg2|nut|ogm|ogv|rm|ts|vob|webm|wmv)$/i;
|
||||
|
||||
|
||||
// extract songs + add play column
|
||||
@@ -9073,6 +9087,15 @@ var arcfmt = (function () {
|
||||
}
|
||||
ebi('selzip').textContent = fmt.split('_')[0];
|
||||
ebi('selzip').setAttribute('fmt', arg);
|
||||
|
||||
QS('#zip1 span').textContent = fmt.split('_')[0];
|
||||
ebi('zip1').setAttribute("href",
|
||||
get_evpath() + (dk ? '?k=' + dk + '&': '?') + arg);
|
||||
|
||||
if (!have_zip) {
|
||||
ebi('zip1').style.display = 'none';
|
||||
ebi('selzip').style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function try_render() {
|
||||
@@ -9311,7 +9334,10 @@ var msel = (function () {
|
||||
r.selui(true);
|
||||
arcfmt.render();
|
||||
fileman.render();
|
||||
ebi('selzip').style.display = is_srch ? 'none' : '';
|
||||
|
||||
var zipvis = (is_srch || !have_zip) ? 'none' : '';
|
||||
ebi('selzip').style.display = zipvis;
|
||||
ebi('zip1').style.display = zipvis;
|
||||
}
|
||||
return r;
|
||||
})();
|
||||
|
||||
@@ -101,6 +101,7 @@
|
||||
gio mount -a dav{{ s }}://{{ ep }}/{{ rvp }}
|
||||
{%- endif %}
|
||||
</pre>
|
||||
<p>on KDE Dolphin, use <code>webdav{{ s }}://{{ ep }}/{{ rvp }}</code></p>
|
||||
</div>
|
||||
|
||||
<div class="os mac">
|
||||
|
||||
@@ -1229,7 +1229,7 @@ function dl_file(url) {
|
||||
function cliptxt(txt, ok) {
|
||||
var fb = function () {
|
||||
console.log('clip-fb');
|
||||
var o = mknod('input');
|
||||
var o = mknod('textarea');
|
||||
o.value = txt;
|
||||
document.body.appendChild(o);
|
||||
o.focus();
|
||||
@@ -1239,6 +1239,8 @@ function cliptxt(txt, ok) {
|
||||
ok();
|
||||
};
|
||||
try {
|
||||
if (!window.isSecureContext)
|
||||
throw 1;
|
||||
navigator.clipboard.writeText(txt).then(ok, fb);
|
||||
}
|
||||
catch (ex) { fb(); }
|
||||
|
||||
@@ -1,3 +1,29 @@
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2025-0426-2149 `v1.17.0` mixtape.m3u
|
||||
|
||||
## 🧪 new features
|
||||
|
||||
* [m3u playlists](https://github.com/9001/copyparty/#playlists) 897f9d32 ad200f2b 4195762d fff45552
|
||||
* create and play m3u / m3u8 files
|
||||
|
||||
## 🩹 bugfixes
|
||||
|
||||
* improve support for ie11 (yes, internet explorer 11) 3090c748 95157d02
|
||||
* now possible to launch the password-hasher cli while another instance is running dbfc899d
|
||||
* in preparation of #157 / #159
|
||||
|
||||
## 🔧 other changes
|
||||
|
||||
* make better decisions when running in a VM with less than 1 GiB RAM dc3b7a27
|
||||
|
||||
## 🌠 fun facts
|
||||
|
||||
* this release contains code written [less than 1masl](https://a.ocv.me/pub/g/nerd-stuff/PXL_20250425_170037812.jpg) and was gonna be named [hash again](https://www.youtube.com/watch?v=twUFbqyul_M) since it was originally just the password-hasher fix, but then kipun suggested adding playlist support (thx kipun)
|
||||
* [donations](https://github.com/9001/) are now also possible through github -- good alternative to paypal (y)
|
||||
* and thanks a lot for the support (and kind words therein) so far, appreciate it :>
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2025-0420-1836 `v1.16.21` unzip-compat
|
||||
|
||||
|
||||
@@ -226,10 +226,13 @@ var tl_browser = {
|
||||
"wt_pst": "paste a previously cut / copied selection$NHotkey: ctrl-V",
|
||||
"wt_selall": "select all files$NHotkey: ctrl-A (when file focused)",
|
||||
"wt_selinv": "invert selection",
|
||||
"wt_zip1": "download this folder as archive",
|
||||
"wt_selzip": "download selection as archive",
|
||||
"wt_seldl": "download selection as separate files$NHotkey: Y",
|
||||
"wt_npirc": "copy irc-formatted track info",
|
||||
"wt_nptxt": "copy plaintext track info",
|
||||
"wt_m3ua": "add to m3u playlist (click <code>📻copy</code> later)",
|
||||
"wt_m3uc": "copy m3u playlist to clipboard",
|
||||
"wt_grid": "toggle grid / list view$NHotkey: G",
|
||||
"wt_prev": "previous track$NHotkey: J",
|
||||
"wt_play": "play / pause$NHotkey: P",
|
||||
@@ -366,6 +369,7 @@ var tl_browser = {
|
||||
"mt_fau": "on phones, prevent music from stopping if the next song doesn't preload fast enough (can make tags display glitchy)\">☕️",
|
||||
"mt_waves": "waveform seekbar:$Nshow audio amplitude in the scrubber\">~s",
|
||||
"mt_npclip": "show buttons for clipboarding the currently playing song\">/np",
|
||||
"mt_m3u_c": "show buttons for clipboarding the$Nselected songs as m3u8 playlist entries\">📻",
|
||||
"mt_octl": "os integration (media hotkeys / osd)\">os-ctl",
|
||||
"mt_oseek": "allow seeking through os integration$N$Nnote: on some devices (iPhones),$Nthis replaces the next-song button\">seek",
|
||||
"mt_oscv": "show album cover in osd\">art",
|
||||
@@ -391,6 +395,7 @@ var tl_browser = {
|
||||
|
||||
"mb_play": "play",
|
||||
"mm_hashplay": "play this audio file?",
|
||||
"mm_m3u": "press <code>Enter/OK</code> to Play\npress <code>ESC/Cancel</code> to Edit",
|
||||
"mp_breq": "need firefox 82+ or chrome 73+ or iOS 15+",
|
||||
"mm_bload": "now loading...",
|
||||
"mm_bconv": "converting to {0}, please wait...",
|
||||
@@ -521,6 +526,10 @@ var tl_browser = {
|
||||
"tvt_sel": "select file ( for cut / copy / delete / ... )$NHotkey: S\">sel",
|
||||
"tvt_edit": "open file in text editor$NHotkey: E\">✏️ edit",
|
||||
|
||||
"m3u_add1": "song added to m3u playlist",
|
||||
"m3u_addn": "{0} songs added to m3u playlist",
|
||||
"m3u_clip": "m3u playlist now copied to clipboard\n\nyou should create a new textfile named something.m3u and paste the playlist in that document; this will make it playable",
|
||||
|
||||
"gt_vau": "don't show videos, just play the audio\">🎧",
|
||||
"gt_msel": "enable file selection; ctrl-click a file to override$N$N<em>when active: doubleclick a file / folder to open it</em>$N$NHotkey: S\">multiselect",
|
||||
"gt_crop": "center-crop thumbnails\">crop",
|
||||
|
||||
@@ -82,6 +82,19 @@ def get_ramdisk():
|
||||
return subdir(vol)
|
||||
|
||||
if os.path.exists("/Volumes"):
|
||||
sck = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
while True:
|
||||
try:
|
||||
sck.bind(("127.0.0.1", 2775))
|
||||
break
|
||||
except:
|
||||
print("waiting for 2775")
|
||||
time.sleep(0.5)
|
||||
|
||||
v = "/Volumes/cptd"
|
||||
if os.path.exists(v):
|
||||
return subdir(v)
|
||||
|
||||
# hdiutil eject /Volumes/cptd/
|
||||
devname, _ = chkcmd("hdiutil attach -nomount ram://131072".split())
|
||||
devname = devname.strip()
|
||||
@@ -97,6 +110,7 @@ def get_ramdisk():
|
||||
except:
|
||||
pass
|
||||
|
||||
sck.close()
|
||||
return subdir("/Volumes/cptd")
|
||||
except Exception as ex:
|
||||
print(repr(ex))
|
||||
@@ -166,6 +180,7 @@ class Cfg(Namespace):
|
||||
v=v or [],
|
||||
c=c,
|
||||
E=E,
|
||||
bup_ck="sha512",
|
||||
dbd="wal",
|
||||
dk_salt="b" * 16,
|
||||
fk_salt="a" * 16,
|
||||
@@ -178,6 +193,8 @@ class Cfg(Namespace):
|
||||
mte={"a": True},
|
||||
mth={},
|
||||
mtp=[],
|
||||
put_ck="sha512",
|
||||
put_name="put-{now.6f}-{cip}.bin",
|
||||
mv_retry="0/0",
|
||||
rm_retry="0/0",
|
||||
s_rd_sz=256 * 1024,
|
||||
|
||||
Reference in New Issue
Block a user