copyparty 1.19.0__py3-none-any.whl → 1.19.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
copyparty/__main__.py CHANGED
@@ -428,6 +428,40 @@ def args_from_cfg(cfg_path ) :
428
428
  return ret
429
429
 
430
430
 
431
+ def expand_cfg(argv) :
432
+ if CFG_DEF:
433
+ supp = args_from_cfg(CFG_DEF[0])
434
+ argv = supp + argv
435
+
436
+ n = spins = 0
437
+ while n < len(argv):
438
+ if not n:
439
+ if spins % 1000 == 999:
440
+ t = "still expanding config files... giving up after %d more"
441
+ print(t % (9999 - spins))
442
+ if spins > 9999:
443
+ t = "got stuck expanding config files; do you have a config-file which imports itself? this is where I gave up:\n%r"
444
+ raise Exception(t % (argv[:1000]))
445
+ v1 = argv[n]
446
+ v1v = v1[2:].lstrip("=")
447
+ try:
448
+ v2 = argv[n + 1]
449
+ except:
450
+ v2 = ""
451
+
452
+ if v1 == "-c" and v2 and os.path.isfile(v2):
453
+ argv = argv[:n] + args_from_cfg(v2) + argv[n + 2 :]
454
+ spins += 1
455
+ n = 0
456
+ elif v1.startswith("-c") and v1v and os.path.isfile(v1v):
457
+ argv = argv[:n] + args_from_cfg(v1v) + argv[n + 1 :]
458
+ spins += 1
459
+ n = 0
460
+ else:
461
+ n += 1
462
+ return argv
463
+
464
+
431
465
  def sighandler(sig = None, frame = None) :
432
466
  msg = [""] * 5
433
467
  for th in threading.enumerate():
@@ -601,8 +635,41 @@ def get_sects():
601
635
  if no accounts or volumes are configured,
602
636
  current folder will be read/write for everyone
603
637
 
638
+ the group @acct will always have every user with an account
639
+ (the name of that group can be changed with --grp-all)
640
+
604
641
  consider the config file for more flexible account/volume management,
605
642
  including dynamic reload at runtime (and being more readable w)
643
+
644
+ see \033[32m--help-auth\033[0m for ways to provide the password in requests;
645
+ see \033[32m--help-idp\033[0m for replacing it with SSO and auth-middlewares
646
+ """
647
+ ),
648
+ ],
649
+ [
650
+ "auth",
651
+ "how to login from a client",
652
+ dedent(
653
+ """
654
+ different ways to provide the password so you become authenticated:
655
+
656
+ login with the ui:
657
+ go to \033[36mhttp://127.0.0.1:3923/?h\033[0m and login there
658
+
659
+ send the password in the '\033[36mPW\033[0m' http-header:
660
+ \033[36mPW: \033[35mhunter2\033[0m
661
+ or if you have \033[33m--accounts\033[0m enabled,
662
+ \033[36mPW: \033[35med:hunter2\033[0m
663
+
664
+ send the password in the URL itself:
665
+ \033[36mhttp://127.0.0.1:3923/\033[35m?pw=hunter2\033[0m
666
+ or if you have \033[33m--accounts\033[0m enabled,
667
+ \033[36mhttp://127.0.0.1:3923/\033[35m?pw=ed:hunter2\033[0m
668
+
669
+ use basic-authentication:
670
+ \033[36mhttp://\033[35med:hunter2\033[36m@127.0.0.1:3923/\033[0m
671
+ which should be the same as this header:
672
+ \033[36mAuthorization: Basic \033[35mZWQ6aHVudGVyMg==\033[0m
606
673
  """
607
674
  ),
608
675
  ],
@@ -754,6 +821,36 @@ def get_sects():
754
821
  the upload speed can easily drop to 10% for small files)"""
755
822
  ),
756
823
  ],
824
+ [
825
+ "idp",
826
+ "replacing the login system with fancy middleware",
827
+ dedent(
828
+ """
829
+ if you already have a centralized service which handles
830
+ user-authentication for other services already, you can
831
+ integrate copyparty with that for automatic login
832
+
833
+ if the middleware is providing the username in an http-header
834
+ named '\033[35mtheUsername\033[0m' then do this: \033[36m--idp-h-usr theUsername\033[0m
835
+
836
+ if the middleware is providing a list of groups in the header
837
+ named '\033[35mtheGroups\033[0m' then do this: \033[36m--idp-h-grp theGroup\033[0m
838
+
839
+ if the list of groups is separated by '\033[35m%\033[0m' then \033[36m--idp-gsep %\033[0m
840
+
841
+ if the middleware is providing a header named '\033[35mAccount\033[0m'
842
+ and the value is '\033[35malice@forest.net\033[0m' but the username is
843
+ actually '\033[35mmarisa\033[0m' then do this for each user:
844
+ \033[36m--idp-hm-usr ^Account^alice@forest.net^marisa\033[0m
845
+ (the separator '\033[35m^\033[0m' can be any character)
846
+
847
+ make ABSOLUTELY SURE that the header can only be set by your
848
+ middleware and not by clients! and, as an extra precaution,
849
+ send a header named '\033[36mfinalmasterspark\033[0m' (a secret keyword)
850
+ and then \033[36m--idp-h-key finalmasterspark\033[0m to require that
851
+ """
852
+ ),
853
+ ],
757
854
  [
758
855
  "urlform",
759
856
  "how to handle url-form POSTs",
@@ -1011,14 +1108,15 @@ def add_general(ap, nc, srvname):
1011
1108
 
1012
1109
  def add_qr(ap, tty):
1013
1110
  ap2 = ap.add_argument_group("qr options")
1014
- ap2.add_argument("--qr", action="store_true", help="show http:// QR-code on startup")
1015
- ap2.add_argument("--qrs", action="store_true", help="show https:// QR-code on startup")
1111
+ ap2.add_argument("--qr", action="store_true", help="show QR-code on startup")
1112
+ ap2.add_argument("--qrs", action="store_true", help="change the QR-code URL to https://")
1016
1113
  ap2.add_argument("--qrl", metavar="PATH", type=u, default="", help="location to include in the url, for example [\033[32mpriv/?pw=hunter2\033[0m]")
1017
1114
  ap2.add_argument("--qri", metavar="PREFIX", type=u, default="", help="select IP which starts with \033[33mPREFIX\033[0m; [\033[32m.\033[0m] to force default IP when mDNS URL would have been used instead")
1018
- ap2.add_argument("--qr-fg", metavar="COLOR", type=int, default=0 if tty else 16, help="foreground; try [\033[32m0\033[0m] if the qr-code is unreadable")
1115
+ ap2.add_argument("--qr-fg", metavar="COLOR", type=int, default=0 if tty else 16, help="foreground; try [\033[32m0\033[0m] or [\033[32m-1\033[0m] if the qr-code is unreadable")
1019
1116
  ap2.add_argument("--qr-bg", metavar="COLOR", type=int, default=229, help="background (white=255)")
1020
1117
  ap2.add_argument("--qrp", metavar="CELLS", type=int, default=4, help="padding (spec says 4 or more, but 1 is usually fine)")
1021
1118
  ap2.add_argument("--qrz", metavar="N", type=int, default=0, help="[\033[32m1\033[0m]=1x, [\033[32m2\033[0m]=2x, [\033[32m0\033[0m]=auto (try [\033[32m2\033[0m] on broken fonts)")
1119
+ ap2.add_argument("--qr-pin", metavar="N", type=int, default=0, help="sticky/pin the qr-code to always stay on-screen; [\033[32m0\033[0m]=disabled, [\033[32m1\033[0m]=with-url, [\033[32m2\033[0m]=just-qr")
1022
1120
 
1023
1121
 
1024
1122
  def add_fs(ap):
@@ -1048,6 +1146,7 @@ def add_upload(ap):
1048
1146
  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)")
1049
1147
  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)")
1050
1148
  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")
1149
+ ap2.add_argument("--unp-who", metavar="NUM", type=int, default=1, help="clients can undo recent 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=unp_who)")
1051
1150
  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)")
1052
1151
  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)")
1053
1152
  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")
@@ -1141,7 +1240,8 @@ def add_auth(ap):
1141
1240
  idp_db = os.path.join(E.cfg, "idp.db")
1142
1241
  ses_db = os.path.join(E.cfg, "sessions.db")
1143
1242
  ap2 = ap.add_argument_group("IdP / identity provider / user authentication options")
1144
- ap2.add_argument("--idp-h-usr", metavar="HN", type=u, default="", help="bypass the copyparty authentication checks if the request-header \033[33mHN\033[0m contains a username to associate the request with (for use with authentik/oauth/...)\n\033[1;31mWARNING:\033[0m if you enable this, make sure clients are unable to specify this header themselves; must be washed away and replaced by a reverse-proxy")
1243
+ ap2.add_argument("--idp-h-usr", metavar="HN", type=u, action="append", help="\033[34mREPEATABLE:\033[0m bypass the copyparty authentication checks if the request-header \033[33mHN\033[0m contains a username to associate the request with (for use with authentik/oauth/...)\n\033[1;31mWARNING:\033[0m if you enable this, make sure clients are unable to specify this header themselves; must be washed away and replaced by a reverse-proxy")
1244
+ ap2.add_argument("--idp-hm-usr", metavar="TXT", type=u, action="append", help="\033[34mREPEATABLE:\033[0m bypass the copyparty authentication checks if the request-header \033[33mHN\033[0m is provided, and its value exists in a mapping defined by this option; see --help-idp")
1145
1245
  ap2.add_argument("--idp-h-grp", metavar="HN", type=u, default="", help="assume the request-header \033[33mHN\033[0m contains the groupname of the requesting user; can be referenced in config files for group-based access control")
1146
1246
  ap2.add_argument("--idp-h-key", metavar="HN", type=u, default="", help="optional but recommended safeguard; your reverse-proxy will insert a secret header named \033[33mHN\033[0m into all requests, and the other IdP headers will be ignored if this header is not present")
1147
1247
  ap2.add_argument("--idp-gsep", metavar="RE", type=u, default="|:;+,", help="if there are multiple groups in \033[33m--idp-h-grp\033[0m, they are separated by one of the characters in \033[33mRE\033[0m")
@@ -1154,7 +1254,11 @@ def add_auth(ap):
1154
1254
  ap2.add_argument("--ses-db", metavar="PATH", type=u, default=ses_db, help="where to store the sessions database (if you run multiple copyparty instances, make sure they use different DBs)")
1155
1255
  ap2.add_argument("--ses-len", metavar="CHARS", type=int, default=20, help="session key length; default is 120 bits ((20//4)*4*6)")
1156
1256
  ap2.add_argument("--no-ses", action="store_true", help="disable sessions; use plaintext passwords in cookies")
1257
+ ap2.add_argument("--grp-all", metavar="NAME", type=u, default="acct", help="the name of the auto-generated group which contains every username which is known")
1157
1258
  ap2.add_argument("--ipu", metavar="CIDR=USR", type=u, action="append", help="\033[34mREPEATABLE:\033[0m users with IP matching \033[33mCIDR\033[0m are auto-authenticated as username \033[33mUSR\033[0m; example: [\033[32m172.16.24.0/24=dave]")
1259
+ ap2.add_argument("--ipr", metavar="CIDR=USR", type=u, action="append", help="\033[34mREPEATABLE:\033[0m username \033[33mUSR\033[0m can only connect from an IP matching one or more \033[33mCIDR\033[0m (comma-sep.); example: [\033[32m192.168.123.0/24,172.16.0.0/16=dave]")
1260
+ ap2.add_argument("--have-idp-hdrs", type=u, default="", help=argparse.SUPPRESS)
1261
+ ap2.add_argument("--have-ipu-or-ipr", type=u, default="", help=argparse.SUPPRESS)
1158
1262
 
1159
1263
 
1160
1264
  def add_chpw(ap):
@@ -1193,6 +1297,7 @@ def add_zc_mdns(ap):
1193
1297
  ap2.add_argument("--zm-lh", metavar="PATH", type=u, default="", help="link a specific folder for http shares")
1194
1298
  ap2.add_argument("--zm-lf", metavar="PATH", type=u, default="", help="link a specific folder for ftp shares")
1195
1299
  ap2.add_argument("--zm-ls", metavar="PATH", type=u, default="", help="link a specific folder for smb shares")
1300
+ ap2.add_argument("--zm-fqdn", metavar="FQDN", type=u, default="--name.local", help="the domain to announce; NOTE: using anything other than .local is nonstandard and could cause problems")
1196
1301
  ap2.add_argument("--zm-mnic", action="store_true", help="merge NICs which share subnets; assume that same subnet means same network")
1197
1302
  ap2.add_argument("--zm-msub", action="store_true", help="merge subnets on each NIC -- always enabled for ipv6 -- reduces network load, but gnome-gvfs clients may stop working, and clients cannot be in subnets that the server is not")
1198
1303
  ap2.add_argument("--zm-noneg", action="store_true", help="disable NSEC replies -- try this if some clients don't see copyparty")
@@ -1309,6 +1414,7 @@ def add_optouts(ap):
1309
1414
  ap2.add_argument("--no-del", action="store_true", help="disable delete operations")
1310
1415
  ap2.add_argument("--no-mv", action="store_true", help="disable move/rename operations")
1311
1416
  ap2.add_argument("--no-cp", action="store_true", help="disable copy operations")
1417
+ ap2.add_argument("--no-fs-abrt", action="store_true", help="disable ability to abort ongoing copy/move")
1312
1418
  ap2.add_argument("-nth", action="store_true", help="no title hostname; don't show \033[33m--name\033[0m in <title>")
1313
1419
  ap2.add_argument("-nih", action="store_true", help="no info hostname -- don't show in UI")
1314
1420
  ap2.add_argument("-nid", action="store_true", help="no info disk-usage -- don't show in UI")
@@ -1330,7 +1436,7 @@ def add_optouts(ap):
1330
1436
  def add_safety(ap):
1331
1437
  ap2 = ap.add_argument_group("safety options")
1332
1438
  ap2.add_argument("-s", action="count", default=0, help="increase safety: Disable thumbnails / potentially dangerous software (ffmpeg/pillow/vips), hide partial uploads, avoid crawlers.\n └─Alias of\033[32m --dotpart --no-thumb --no-mtag-ff --no-robots --force-js")
1333
- ap2.add_argument("-ss", action="store_true", help="further increase safety: Prevent js-injection, accidental move/delete, broken symlinks, webdav, 404 on 403, ban on excessive 404s.\n └─Alias of\033[32m -s --unpost=0 --no-del --no-mv --hardlink --vague-403 -nih")
1439
+ ap2.add_argument("-ss", action="store_true", help="further increase safety: Prevent js-injection, accidental move/delete, broken symlinks, webdav requires login, 404 on 403, ban on excessive 404s.\n └─Alias of\033[32m -s --unpost=0 --no-del --no-mv --hardlink --dav-auth --vague-403 -nih")
1334
1440
  ap2.add_argument("-sss", action="store_true", help="further increase safety: Enable logging to disk, scan for dangerous symlinks.\n └─Alias of\033[32m -ss --no-dav --no-logues --no-readme -lo=cpp-%%Y-%%m%%d-%%H%%M%%S.txt.xz --ls=**,*,ln,p,r")
1335
1441
  ap2.add_argument("--ls", metavar="U[,V[,F]]", type=u, default="", help="do a sanity/safety check of all volumes on startup; arguments \033[33mUSER\033[0m,\033[33mVOL\033[0m,\033[33mFLAGS\033[0m (see \033[33m--help-ls\033[0m); example [\033[32m**,*,ln,p,r\033[0m]")
1336
1442
  ap2.add_argument("--xvol", action="store_true", help="never follow symlinks leaving the volume root, unless the link is into another volume where the user has similar access (volflag=xvol)")
@@ -1352,6 +1458,8 @@ def add_safety(ap):
1352
1458
  ap2.add_argument("--sus-urls", metavar="R", type=u, default=r"\.php$|(^|/)wp-(admin|content|includes)/", help="URLs which are considered sus / eligible for banning; disable with blank or [\033[32mno\033[0m]")
1353
1459
  ap2.add_argument("--nonsus-urls", metavar="R", type=u, default=r"^(favicon\.ico|robots\.txt)$|^apple-touch-icon|^\.well-known", help="harmless URLs ignored from 404-bans; disable with blank or [\033[32mno\033[0m]")
1354
1460
  ap2.add_argument("--early-ban", action="store_true", help="if a client is banned, reject its connection as soon as possible; not a good idea to enable when proxied behind cloudflare since it could ban your reverse-proxy")
1461
+ ap2.add_argument("--cookie-nmax", metavar="N", type=int, default=50, help="reject HTTP-request from client if they send more than N cookies")
1462
+ ap2.add_argument("--cookie-cmax", metavar="N", type=int, default=8192, help="reject HTTP-request from client if more than N characters in Cookie header")
1355
1463
  ap2.add_argument("--aclose", metavar="MIN", type=int, default=10, help="if a client maxes out the server connection limit, downgrade it from connection:keep-alive to connection:close for \033[33mMIN\033[0m minutes (and also kill its active connections) -- disable with 0")
1356
1464
  ap2.add_argument("--loris", metavar="B", type=int, default=60, help="if a client maxes out the server connection limit without sending headers, ban it for \033[33mB\033[0m minutes; disable with [\033[32m0\033[0m]")
1357
1465
  ap2.add_argument("--acao", metavar="V[,V]", type=u, default="*", help="Access-Control-Allow-Origin; list of origins (domains/IPs without port) to accept requests from; [\033[32mhttps://1.2.3.4\033[0m]. Default [\033[32m*\033[0m] allows requests from all sites but removes cookies and http-auth; only ?pw=hunter2 survives")
@@ -1382,7 +1490,7 @@ def add_shutdown(ap):
1382
1490
  def add_logging(ap):
1383
1491
  ap2 = ap.add_argument_group("logging options")
1384
1492
  ap2.add_argument("-q", action="store_true", help="quiet; disable most STDOUT messages")
1385
- ap2.add_argument("-lo", metavar="PATH", type=u, default="", help="logfile, example: \033[32mcpp-%%Y-%%m%%d-%%H%%M%%S.txt.xz\033[0m (NB: some errors may appear on STDOUT only)")
1493
+ ap2.add_argument("-lo", metavar="PATH", type=u, default="", help="logfile; use .txt for plaintext or .xz for compressed. Example: \033[32mcpp-%%Y-%%m%%d-%%H%%M%%S.txt.xz\033[0m (NB: some errors may appear on STDOUT only)")
1386
1494
  ap2.add_argument("--no-ansi", action="store_true", default=not VT100, help="disable colors; same as environment-variable NO_COLOR")
1387
1495
  ap2.add_argument("--ansi", action="store_true", help="force colors; overrides environment-variable NO_COLOR")
1388
1496
  ap2.add_argument("--no-logflush", action="store_true", help="don't flush the logfile after each write; tiny bit faster")
@@ -1418,11 +1526,12 @@ def add_thumbnail(ap):
1418
1526
  ap2.add_argument("--no-athumb", action="store_true", help="disable audio thumbnails (spectrograms) (volflag=dathumb)")
1419
1527
  ap2.add_argument("--th-size", metavar="WxH", default="320x256", help="thumbnail res (volflag=thsize)")
1420
1528
  ap2.add_argument("--th-mt", metavar="CORES", type=int, default=CORES, help="num cpu cores to use for generating thumbnails")
1421
- ap2.add_argument("--th-convt", metavar="SEC", type=float, default=60.0, help="conversion timeout in seconds (volflag=convt)")
1529
+ ap2.add_argument("--th-convt", metavar="SEC", type=float, default=60.0, help="convert-to-image timeout in seconds (volflag=convt)")
1530
+ ap2.add_argument("--ac-convt", metavar="SEC", type=float, default=150.0, help="convert-to-audio timeout in seconds (volflag=aconvt)")
1422
1531
  ap2.add_argument("--th-ram-max", metavar="GB", type=float, default=th_ram, help="max memory usage (GiB) permitted by thumbnailer; not very accurate")
1423
1532
  ap2.add_argument("--th-crop", metavar="TXT", type=u, default="y", help="crop thumbnails to 4:3 or keep dynamic height; client can override in UI unless force. [\033[32my\033[0m]=crop, [\033[32mn\033[0m]=nocrop, [\033[32mfy\033[0m]=force-y, [\033[32mfn\033[0m]=force-n (volflag=crop)")
1424
1533
  ap2.add_argument("--th-x3", metavar="TXT", type=u, default="n", help="show thumbs at 3x resolution; client can override in UI unless force. [\033[32my\033[0m]=yes, [\033[32mn\033[0m]=no, [\033[32mfy\033[0m]=force-yes, [\033[32mfn\033[0m]=force-no (volflag=th3x)")
1425
- ap2.add_argument("--th-dec", metavar="LIBS", default="vips,pil,ff", help="image decoders, in order of preference")
1534
+ ap2.add_argument("--th-dec", metavar="LIBS", default="vips,pil,raw,ff", help="image decoders, in order of preference")
1426
1535
  ap2.add_argument("--th-no-jpg", action="store_true", help="disable jpg output")
1427
1536
  ap2.add_argument("--th-no-webp", action="store_true", help="disable webp output")
1428
1537
  ap2.add_argument("--th-ff-jpg", action="store_true", help="force jpg output for video thumbs (avoids issues on some FFmpeg builds)")
@@ -1431,16 +1540,19 @@ def add_thumbnail(ap):
1431
1540
  ap2.add_argument("--th-clean", metavar="SEC", type=int, default=43200, help="cleanup interval; 0=disabled")
1432
1541
  ap2.add_argument("--th-maxage", metavar="SEC", type=int, default=604800, help="max folder age -- folders which haven't been poked for longer than \033[33m--th-poke\033[0m seconds will get deleted every \033[33m--th-clean\033[0m seconds")
1433
1542
  ap2.add_argument("--th-covers", metavar="N,N", type=u, default="folder.png,folder.jpg,cover.png,cover.jpg", help="folder thumbnails to stat/look for; enabling \033[33m-e2d\033[0m will make these case-insensitive, and try them as dotfiles (.folder.jpg), and also automatically select thumbnails for all folders that contain pics, even if none match this pattern")
1543
+ ap2.add_argument("--th-spec-p", metavar="N", type=u, default=1, help="for music, do spectrograms or embedded coverart? [\033[32m0\033[0m]=only-art, [\033[32m1\033[0m]=prefer-art, [\033[32m2\033[0m]=only-spec")
1434
1544
  # https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html
1435
1545
  # https://github.com/libvips/libvips
1546
+ # https://stackoverflow.com/a/47612661
1436
1547
  # ffmpeg -hide_banner -demuxers | awk '/^ D /{print$2}' | while IFS= read -r x; do ffmpeg -hide_banner -h demuxer=$x; done | grep -E '^Demuxer |extensions:'
1437
- ap2.add_argument("--th-r-pil", metavar="T,T", type=u, default="avif,avifs,blp,bmp,cbz,dcx,dds,dib,emf,eps,fits,flc,fli,fpx,gif,heic,heics,heif,heifs,icns,ico,im,j2p,j2k,jp2,jpeg,jpg,jpx,pbm,pcx,pgm,png,pnm,ppm,psd,qoi,sgi,spi,tga,tif,tiff,webp,wmf,xbm,xpm", help="image formats to decode using pillow")
1438
- 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")
1439
- 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")
1548
+ ap2.add_argument("--th-r-pil", metavar="T,T", type=u, default="avif,avifs,blp,bmp,cbz,dcx,dds,dib,emf,eps,epub,fits,flc,fli,fpx,gif,heic,heics,heif,heifs,icns,ico,im,j2p,j2k,jp2,jpeg,jpg,jpx,pbm,pcx,pgm,png,pnm,ppm,psd,qoi,sgi,spi,tga,tif,tiff,webp,wmf,xbm,xpm", help="image formats to decode using pillow")
1549
+ ap2.add_argument("--th-r-vips", metavar="T,T", type=u, default="avif,exr,fit,fits,fts,gif,hdr,heic,heics,heif,heifs,jp2,jpeg,jpg,jpx,jxl,nii,pfm,pgm,png,ppm,svg,tif,tiff,webp", help="image formats to decode using pyvips")
1550
+ ap2.add_argument("--th-r-raw", metavar="T,T", type=u, default="arw,cr2,cr3,crw,dcr,dng,erf,k25,kdc,mrw,nef,orf,pef,raf,raw,sr2,srf,x3f", help="image formats to decode using rawpy")
1551
+ ap2.add_argument("--th-r-ffi", metavar="T,T", type=u, default="apng,avif,avifs,bmp,cbz,dds,dib,epub,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")
1440
1552
  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")
1441
1553
  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")
1442
1554
  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)")
1443
- 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")
1555
+ 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, epub=jpg.epub", help="audio/image formats to decompress before passing to ffmpeg")
1444
1556
 
1445
1557
 
1446
1558
  def add_transcoding(ap):
@@ -1485,8 +1597,8 @@ def add_db_general(ap, hcores):
1485
1597
  ap2.add_argument("-e2vp", action="store_true", help="on hash mismatch: panic and quit copyparty")
1486
1598
  ap2.add_argument("--hist", metavar="PATH", type=u, default="", help="where to store volume data (db, thumbs); default is a folder named \".hist\" inside each volume (volflag=hist)")
1487
1599
  ap2.add_argument("--dbpath", metavar="PATH", type=u, default="", help="override where the volume databases are to be placed; default is the same as \033[33m--hist\033[0m (volflag=dbpath)")
1488
- ap2.add_argument("--no-hash", metavar="PTN", type=u, default="", help="regex: disable hashing of matching absolute-filesystem-paths during e2ds folder scans (volflag=nohash)")
1489
- ap2.add_argument("--no-idx", metavar="PTN", type=u, default=noidx, help="regex: disable indexing of matching absolute-filesystem-paths during e2ds folder scans (volflag=noidx)")
1600
+ ap2.add_argument("--no-hash", metavar="PTN", type=u, default="", help="regex: disable hashing of matching absolute-filesystem-paths during e2ds folder scans (must be specified as one big regex, not multiple times) (volflag=nohash)")
1601
+ ap2.add_argument("--no-idx", metavar="PTN", type=u, default=noidx, help="regex: disable indexing of matching absolute-filesystem-paths during e2ds folder scan (must be specified as one big regex, not multiple times) (volflag=noidx)")
1490
1602
  ap2.add_argument("--no-dirsz", action="store_true", help="do not show total recursive size of folders in listings, show inode size instead; slightly faster (volflag=nodirsz)")
1491
1603
  ap2.add_argument("--re-dirsz", action="store_true", help="if the directory-sizes in the UI are bonkers, use this along with \033[33m-e2dsa\033[0m to rebuild the index from scratch")
1492
1604
  ap2.add_argument("--no-dhash", action="store_true", help="disable rescan acceleration; do full database integrity check -- makes the db ~5%% smaller and bootup/rescans 3~10x slower")
@@ -1552,13 +1664,14 @@ def add_og(ap):
1552
1664
 
1553
1665
 
1554
1666
  def add_ui(ap, retry):
1667
+ THEMES = 10
1555
1668
  ap2 = ap.add_argument_group("ui options")
1556
1669
  ap2.add_argument("--grid", action="store_true", help="show grid/thumbnails by default (volflag=grid)")
1557
1670
  ap2.add_argument("--gsel", action="store_true", help="select files in grid by ctrl-click (volflag=gsel)")
1558
1671
  ap2.add_argument("--localtime", action="store_true", help="default to local timezone instead of UTC")
1559
- ap2.add_argument("--lang", metavar="LANG", type=u, default="eng", help="language; one of the following: \033[32meng nor chi\033[0m")
1560
- ap2.add_argument("--theme", metavar="NUM", type=int, default=0, help="default theme to use (0..7)")
1561
- ap2.add_argument("--themes", metavar="NUM", type=int, default=8, help="number of themes installed")
1672
+ ap2.add_argument("--lang", metavar="LANG", type=u, default="eng", help="language, for example \033[32meng\033[0m / \033[32mnor\033[0m / ...")
1673
+ ap2.add_argument("--theme", metavar="NUM", type=int, default=0, help="default theme to use (0..%d)" % (THEMES - 1,))
1674
+ ap2.add_argument("--themes", metavar="NUM", type=int, default=THEMES, help="number of themes installed")
1562
1675
  ap2.add_argument("--au-vol", metavar="0-100", type=int, default=50, choices=range(0, 101), help="default audio/video volume percent")
1563
1676
  ap2.add_argument("--sort", metavar="C,C,C", type=u, default="href", help="default sort order, comma-separated column IDs (see header tooltips), prefix with '-' for descending. Examples: \033[32mhref -href ext sz ts tags/Album tags/.tn\033[0m (volflag=sort)")
1564
1677
  ap2.add_argument("--nsort", action="store_true", help="default-enable natural sort of filenames with leading numbers (volflag=nsort)")
@@ -1759,16 +1872,7 @@ def main(argv = None) :
1759
1872
 
1760
1873
  ensure_webdeps()
1761
1874
 
1762
- for k, v in zip(argv[1:], argv[2:]):
1763
- if k == "-c" and os.path.isfile(v):
1764
- supp = args_from_cfg(v)
1765
- argv.extend(supp)
1766
-
1767
- for k in argv[1:]:
1768
- v = k[2:]
1769
- if k.startswith("-c") and v and os.path.isfile(v):
1770
- supp = args_from_cfg(v)
1771
- argv.extend(supp)
1875
+ argv = expand_cfg(argv)
1772
1876
 
1773
1877
  deprecated = [
1774
1878
  ("--salt", "--warksalt"),
copyparty/__version__.py CHANGED
@@ -1,8 +1,8 @@
1
1
  # coding: utf-8
2
2
 
3
- VERSION = (1, 19, 0)
3
+ VERSION = (1, 19, 2)
4
4
  CODENAME = "usernames"
5
- BUILD_DT = (2025, 8, 7)
5
+ BUILD_DT = (2025, 8, 17)
6
6
 
7
7
  S_VERSION = ".".join(map(str, VERSION))
8
8
  S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)
copyparty/authsrv.py CHANGED
@@ -874,6 +874,15 @@ class VFS(object):
874
874
  return None
875
875
 
876
876
  if "xvol" in self.flags:
877
+ self_ap = self.realpath + os.sep
878
+ if aps.startswith(self_ap):
879
+ vp = aps[len(self_ap) :]
880
+ if ANYWIN:
881
+ vp = vp.replace(os.sep, "/")
882
+ vn2, _ = self._find(vp)
883
+ if self == vn2:
884
+ return self
885
+
877
886
  all_aps = self.shr_all_aps or self.root.all_aps
878
887
 
879
888
  for vap, vns in all_aps:
@@ -1091,6 +1100,9 @@ class AuthSrv(object):
1091
1100
  if rejected:
1092
1101
  continue
1093
1102
 
1103
+ if gn == self.args.grp_all:
1104
+ gn = ""
1105
+
1094
1106
  # if ap/vp has a user/group placeholder, make sure to keep
1095
1107
  # track so the same user/group is mapped when setting perms;
1096
1108
  # otherwise clear un/gn to indicate it's a regular volume
@@ -1200,6 +1212,7 @@ class AuthSrv(object):
1200
1212
  self.load_idp_db(bool(self.idp_accs))
1201
1213
  ret = {un: gns[:] for un, gns in self.idp_accs.items()}
1202
1214
  ret.update({zs: [""] for zs in acct if zs not in ret})
1215
+ grps[self.args.grp_all] = list(ret.keys())
1203
1216
  for gn, uns in grps.items():
1204
1217
  for un in uns:
1205
1218
  try:
@@ -1677,6 +1690,9 @@ class AuthSrv(object):
1677
1690
  self.log("\n{0}\n{1}{0}".format(t, "\n".join(slns)))
1678
1691
  raise
1679
1692
 
1693
+ self.args.have_idp_hdrs = bool(self.args.idp_h_usr or self.args.idp_hm_usr)
1694
+ self.args.have_ipu_or_ipr = bool(self.args.ipu or self.args.ipr)
1695
+
1680
1696
  self.setup_pwhash(acct)
1681
1697
  defpw = acct.copy()
1682
1698
  self.setup_chpw(acct)
@@ -1689,7 +1705,7 @@ class AuthSrv(object):
1689
1705
 
1690
1706
  mount = cased
1691
1707
 
1692
- if not mount and not self.args.idp_h_usr:
1708
+ if not mount and not self.args.have_idp_hdrs:
1693
1709
  # -h says our defaults are CWD at root and read/write for everyone
1694
1710
  axs = AXS(["*"], ["*"], None, None)
1695
1711
  ehint = ""
@@ -1861,7 +1877,7 @@ class AuthSrv(object):
1861
1877
 
1862
1878
  if missing_users:
1863
1879
  zs = ", ".join(k for k in sorted(missing_users))
1864
- if self.args.idp_h_usr:
1880
+ if self.args.have_idp_hdrs:
1865
1881
  t = "the following users are unknown, and assumed to come from IdP: "
1866
1882
  self.log(t + zs, c=6)
1867
1883
  else:
@@ -1872,6 +1888,16 @@ class AuthSrv(object):
1872
1888
  if LEELOO_DALLAS in all_users:
1873
1889
  raise Exception("sorry, reserved username: " + LEELOO_DALLAS)
1874
1890
 
1891
+ zsl = []
1892
+ for usr in list(acct)[:]:
1893
+ zs = acct[usr].strip()
1894
+ if not zs:
1895
+ zs = ub64enc(os.urandom(48)).decode("ascii")
1896
+ zsl.append(usr)
1897
+ acct[usr] = zs
1898
+ if zsl:
1899
+ self.log("generated random passwords for users %r" % (zsl,), 6)
1900
+
1875
1901
  seenpwds = {}
1876
1902
  for usr, pwd in acct.items():
1877
1903
  if pwd in seenpwds:
@@ -2192,12 +2218,12 @@ class AuthSrv(object):
2192
2218
  if vf not in vol.flags:
2193
2219
  vol.flags[vf] = getattr(self.args, ga)
2194
2220
 
2195
- zs = "forget_ip gid nrand tail_who u2abort u2ow uid ups_who zip_who"
2221
+ zs = "forget_ip gid nrand tail_who th_spec_p u2abort u2ow uid unp_who ups_who zip_who"
2196
2222
  for k in zs.split():
2197
2223
  if k in vol.flags:
2198
2224
  vol.flags[k] = int(vol.flags[k])
2199
2225
 
2200
- zs = "convt tail_fd tail_rate tail_tmax"
2226
+ zs = "aconvt convt tail_fd tail_rate tail_tmax"
2201
2227
  for k in zs.split():
2202
2228
  if k in vol.flags:
2203
2229
  vol.flags[k] = float(vol.flags[k])
@@ -2528,7 +2554,7 @@ class AuthSrv(object):
2528
2554
  if not self.args.no_voldump:
2529
2555
  self.log(t)
2530
2556
 
2531
- if have_e2d or self.args.idp_h_usr:
2557
+ if have_e2d or self.args.have_idp_hdrs:
2532
2558
  t = self.chk_sqlite_threadsafe()
2533
2559
  if t:
2534
2560
  self.log("\n\033[{}\033[0m\n".format(t))
@@ -2817,7 +2843,7 @@ class AuthSrv(object):
2817
2843
  def load_idp_db(self, quiet=False) :
2818
2844
  # mutex me
2819
2845
  level = self.args.idp_store
2820
- if level < 2 or not self.args.idp_h_usr:
2846
+ if level < 2 or not self.args.have_idp_hdrs:
2821
2847
  return
2822
2848
 
2823
2849
 
@@ -2872,7 +2898,7 @@ class AuthSrv(object):
2872
2898
  n = []
2873
2899
  q = "insert into us values (?,?,?)"
2874
2900
  accs = list(self.acct)
2875
- if self.args.idp_h_usr and self.args.idp_cookie:
2901
+ if self.args.have_idp_hdrs and self.args.idp_cookie:
2876
2902
  accs.extend(self.idp_accs.keys())
2877
2903
  for uname in accs:
2878
2904
  if uname not in ases:
copyparty/cfg.py CHANGED
@@ -68,6 +68,7 @@ def vf_bmap() :
68
68
  def vf_vmap() :
69
69
  """argv-to-volflag: simple values"""
70
70
  ret = {
71
+ "ac_convt": "aconvt",
71
72
  "no_hash": "nohash",
72
73
  "no_idx": "noidx",
73
74
  "re_maxage": "scan",
@@ -111,12 +112,14 @@ def vf_vmap() :
111
112
  "tail_tmax",
112
113
  "tail_who",
113
114
  "tcolor",
115
+ "th_spec_p",
114
116
  "txt_eol",
115
117
  "unlist",
116
118
  "u2abort",
117
119
  "u2ts",
118
120
  "uid",
119
121
  "gid",
122
+ "unp_who",
120
123
  "ups_who",
121
124
  "zip_who",
122
125
  "zipmaxn",
@@ -260,7 +263,9 @@ flagcats = {
260
263
  "thsize": "thumbnail res; WxH",
261
264
  "crop": "center-cropping (y/n/fy/fn)",
262
265
  "th3x": "3x resolution (y/n/fy/fn)",
263
- "convt": "conversion timeout in seconds",
266
+ "convt": "convert-to-image timeout in seconds",
267
+ "aconvt": "convert-to-audio timeout in seconds",
268
+ "th_spec_p=1": "make spectrograms? 0=never 1=fallback 2=always",
264
269
  "ext_th=s=/b.png": "use /b.png as thumbnail for file-extension s",
265
270
  },
266
271
  "handlers\n(better explained in --help-handlers)": {
@@ -341,6 +346,7 @@ flagcats = {
341
346
  "dky": 'allow seeing files (not folders) inside a specific folder\nwith "g" perm, and does not require a valid dirkey to do so',
342
347
  "rss": "allow '?rss' URL suffix (experimental)",
343
348
  "rmagic": "expensive analysis for mimetype accuracy",
349
+ "unp_who=2": "unpost only if same... 1=ip+name, 2=ip, 3=name",
344
350
  "ups_who=2": "restrict viewing the list of recent uploads",
345
351
  "zip_who=2": "restrict access to download-as-zip/tar",
346
352
  "zipmaxn=9k": "reject download-as-zip if more than 9000 files",
copyparty/dxml.py CHANGED
@@ -61,6 +61,9 @@ DXMLParser = _DXMLParser
61
61
 
62
62
 
63
63
  def parse_xml(txt ) :
64
+ """
65
+ Parse XML into an xml.etree.ElementTree.Element while defusing some unsafe parts.
66
+ """
64
67
  parser = DXMLParser()
65
68
  parser.feed(txt)
66
69
  return parser.close() # type: ignore
copyparty/ftpd.py CHANGED
@@ -92,6 +92,10 @@ class FtpAuth(DummyAuthorizer):
92
92
 
93
93
  if args.ipu and uname == "*":
94
94
  uname = args.ipu_iu[args.ipu_nm.map(ip)]
95
+ if args.ipr and uname in args.ipr_u:
96
+ if not args.ipr_u[uname].map(ip):
97
+ logging.warning("username [%s] rejected by --ipr", uname)
98
+ uname = "*"
95
99
 
96
100
  if not uname or not (asrv.vfs.aread.get(uname) or asrv.vfs.awrite.get(uname)):
97
101
  g = self.hub.gpwd
@@ -281,9 +285,12 @@ class FtpFs(AbstractedFS):
281
285
  # returning 550 is library-default and suitable
282
286
  raise FSE("No such file or directory")
283
287
 
284
- avfs = vfs.chk_ap(ap, st)
285
- if not avfs:
286
- raise FSE("Permission denied", 1)
288
+ if vfs.realpath:
289
+ avfs = vfs.chk_ap(ap, st)
290
+ if not avfs:
291
+ raise FSE("Permission denied", 1)
292
+ else:
293
+ avfs = vfs
287
294
 
288
295
  self.cwd = nwd
289
296
  (
@@ -398,8 +405,12 @@ class FtpFs(AbstractedFS):
398
405
  return st
399
406
 
400
407
  def utime(self, path , timeval ) :
401
- ap = self.rv2a(path, w=True)[0]
402
- return bos.utime(ap, (timeval, timeval))
408
+ try:
409
+ ap = self.rv2a(path, w=True)[0]
410
+ return bos.utime(ap, (int(time.time()), int(timeval)))
411
+ except Exception as ex:
412
+ logging.error("ftp.utime: %s, %r", ex, ex)
413
+ raise
403
414
 
404
415
  def lstat(self, path ) :
405
416
  ap = self.rv2a(path)[0]
@@ -488,7 +499,11 @@ class FtpHandler(FTPHandler):
488
499
  def ftp_STOR(self, file , mode = "w") :
489
500
  # Optional[str]
490
501
  vp = join(self.fs.cwd, file).lstrip("/")
491
- ap, vfs, rem = self.fs.v2a(vp, w=True)
502
+ try:
503
+ ap, vfs, rem = self.fs.v2a(vp, w=True)
504
+ except Exception as ex:
505
+ self.respond("550 %s" % (ex,), logging.info)
506
+ return
492
507
  self.vfs_map[ap] = vp
493
508
  xbu = vfs.flags.get("xbu")
494
509
  if xbu and not runhook(