copyparty 1.19.7__py3-none-any.whl → 1.19.9__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.
Files changed (67) hide show
  1. copyparty/__init__.py +2 -0
  2. copyparty/__main__.py +60 -27
  3. copyparty/__version__.py +2 -2
  4. copyparty/authsrv.py +195 -14
  5. copyparty/bos/bos.py +38 -2
  6. copyparty/cfg.py +6 -0
  7. copyparty/ftpd.py +7 -25
  8. copyparty/httpcli.py +84 -34
  9. copyparty/httpsrv.py +1 -1
  10. copyparty/mdns.py +3 -1
  11. copyparty/mtag.py +0 -1
  12. copyparty/smbd.py +1 -1
  13. copyparty/svchub.py +38 -3
  14. copyparty/tcpsrv.py +4 -0
  15. copyparty/u2idx.py +29 -4
  16. copyparty/up2k.py +63 -29
  17. copyparty/util.py +49 -24
  18. copyparty/web/a/partyfuse.py +19 -11
  19. copyparty/web/a/u2c.py +6 -4
  20. copyparty/web/baguettebox.js.gz +0 -0
  21. copyparty/web/browser.css.gz +0 -0
  22. copyparty/web/browser.html +1 -1
  23. copyparty/web/browser.js.gz +0 -0
  24. copyparty/web/dbg-audio.js.gz +0 -0
  25. copyparty/web/deps/busy.mp3.gz +0 -0
  26. copyparty/web/deps/easymde.css.gz +0 -0
  27. copyparty/web/deps/easymde.js.gz +0 -0
  28. copyparty/web/deps/marked.js.gz +0 -0
  29. copyparty/web/deps/mini-fa.css.gz +0 -0
  30. copyparty/web/deps/prism.css.gz +0 -0
  31. copyparty/web/deps/prism.js.gz +0 -0
  32. copyparty/web/deps/prismd.css.gz +0 -0
  33. copyparty/web/deps/scp.woff2 +0 -0
  34. copyparty/web/deps/sha512.ac.js.gz +0 -0
  35. copyparty/web/idp.html +5 -5
  36. copyparty/web/md.css.gz +0 -0
  37. copyparty/web/md.html +1 -1
  38. copyparty/web/md.js.gz +0 -0
  39. copyparty/web/md2.css.gz +0 -0
  40. copyparty/web/md2.js.gz +0 -0
  41. copyparty/web/mde.css.gz +0 -0
  42. copyparty/web/mde.html +1 -1
  43. copyparty/web/mde.js.gz +0 -0
  44. copyparty/web/msg.css.gz +0 -0
  45. copyparty/web/msg.html +1 -1
  46. copyparty/web/rups.css.gz +0 -0
  47. copyparty/web/rups.html +1 -1
  48. copyparty/web/rups.js.gz +0 -0
  49. copyparty/web/shares.css.gz +0 -0
  50. copyparty/web/shares.html +5 -5
  51. copyparty/web/shares.js.gz +0 -0
  52. copyparty/web/splash.css.gz +0 -0
  53. copyparty/web/splash.html +54 -38
  54. copyparty/web/splash.js.gz +0 -0
  55. copyparty/web/svcs.html +80 -39
  56. copyparty/web/svcs.js.gz +0 -0
  57. copyparty/web/ui.css.gz +0 -0
  58. copyparty/web/up2k.js.gz +0 -0
  59. copyparty/web/util.js.gz +0 -0
  60. copyparty/web/w.hash.js.gz +0 -0
  61. {copyparty-1.19.7.dist-info → copyparty-1.19.9.dist-info}/METADATA +12 -1
  62. copyparty-1.19.9.dist-info/RECORD +113 -0
  63. copyparty-1.19.7.dist-info/RECORD +0 -113
  64. {copyparty-1.19.7.dist-info → copyparty-1.19.9.dist-info}/WHEEL +0 -0
  65. {copyparty-1.19.7.dist-info → copyparty-1.19.9.dist-info}/entry_points.txt +0 -0
  66. {copyparty-1.19.7.dist-info → copyparty-1.19.9.dist-info}/licenses/LICENSE +0 -0
  67. {copyparty-1.19.7.dist-info → copyparty-1.19.9.dist-info}/top_level.txt +0 -0
copyparty/__init__.py CHANGED
@@ -108,7 +108,9 @@ class EnvParams(object):
108
108
  def __init__(self) :
109
109
  self.t0 = time.time()
110
110
  self.mod = ""
111
+ self.mod_ = ""
111
112
  self.cfg = ""
113
+ self.scfg = True
112
114
 
113
115
 
114
116
  E = EnvParams()
copyparty/__main__.py CHANGED
@@ -36,6 +36,7 @@ from .__init__ import (
36
36
  )
37
37
  from .__version__ import CODENAME, S_BUILD_DT, S_VERSION
38
38
  from .authsrv import expand_config_file, split_cfg_ln, upgrade_cfg_fmt
39
+ from .bos import bos
39
40
  from .cfg import flagcats, onedash
40
41
  from .svchub import SvcHub
41
42
  from .util import (
@@ -180,7 +181,7 @@ def init_E(EE ) :
180
181
 
181
182
  E = EE # pylint: disable=redefined-outer-name
182
183
 
183
- def get_unixdir() :
184
+ def get_unixdir() :
184
185
  paths = [
185
186
  (os.environ.get, "XDG_CONFIG_HOME"),
186
187
  (os.path.expanduser, "~/.config"),
@@ -191,6 +192,8 @@ def init_E(EE ) :
191
192
  ]
192
193
  errs = []
193
194
  for npath, (pf, pa) in enumerate(paths):
195
+ priv = npath < 2 # private/trusted location
196
+ ram = npath > 1 # "nonvolatile"; not semantically same as `not priv`
194
197
  p = ""
195
198
  try:
196
199
  p = pf(pa)
@@ -200,15 +203,21 @@ def init_E(EE ) :
200
203
  p = os.path.normpath(p)
201
204
  mkdir = not os.path.isdir(p)
202
205
  if mkdir:
203
- os.mkdir(p)
206
+ os.mkdir(p, 0o700)
204
207
 
205
208
  p = os.path.join(p, "copyparty")
209
+ if not priv and os.path.isdir(p):
210
+ uid = os.geteuid()
211
+ if os.stat(p).st_uid != uid:
212
+ p += ".%s" % (uid,)
213
+ if os.path.isdir(p) and os.stat(p).st_uid != uid:
214
+ raise Exception("filesystem has broken unix permissions")
206
215
  try:
207
216
  os.listdir(p)
208
217
  except:
209
- os.mkdir(p)
218
+ os.mkdir(p, 0o700)
210
219
 
211
- if npath > 1:
220
+ if ram:
212
221
  t = "Using %s/copyparty [%s] for config; filekeys/dirkeys will change on every restart. Consider setting XDG_CONFIG_HOME or giving the unix-user a ~/.config/"
213
222
  errs.append(t % (pa, p))
214
223
  elif mkdir:
@@ -220,17 +229,19 @@ def init_E(EE ) :
220
229
  if errs:
221
230
  warn(". ".join(errs))
222
231
 
223
- return p # type: ignore
232
+ return p, priv
224
233
  except Exception as ex:
225
- if p and npath < 2:
234
+ if p:
226
235
  t = "Unable to store config in %s [%s] due to %r"
227
236
  errs.append(t % (pa, p, ex))
228
237
 
229
- raise Exception("could not find a writable path for config")
238
+ t = "could not find a writable path for runtime state:\n> %s"
239
+ raise Exception(t % ("\n> ".join(errs)))
230
240
 
231
241
  E.mod = os.path.dirname(os.path.realpath(__file__))
232
242
  if E.mod.endswith("__init__"):
233
243
  E.mod = os.path.dirname(E.mod)
244
+ E.mod_ = os.path.join(E.mod, "")
234
245
 
235
246
  try:
236
247
  p = os.environ.get("XDG_CONFIG_HOME")
@@ -241,7 +252,7 @@ def init_E(EE ) :
241
252
  p = os.path.abspath(os.path.realpath(p))
242
253
  p = os.path.join(p, "copyparty")
243
254
  if not os.path.isdir(p):
244
- os.mkdir(p)
255
+ os.mkdir(p, 0o700)
245
256
  os.listdir(p)
246
257
  except:
247
258
  p = ""
@@ -254,11 +265,11 @@ def init_E(EE ) :
254
265
  elif sys.platform == "darwin":
255
266
  E.cfg = os.path.expanduser("~/Library/Preferences/copyparty")
256
267
  else:
257
- E.cfg = get_unixdir()
268
+ E.cfg, E.scfg = get_unixdir()
258
269
 
259
270
  E.cfg = E.cfg.replace("\\", "/")
260
271
  try:
261
- os.makedirs(E.cfg)
272
+ bos.makedirs(E.cfg, bos.MKD_700)
262
273
  except:
263
274
  if not os.path.isdir(E.cfg):
264
275
  raise
@@ -879,6 +890,11 @@ def get_sects():
879
890
  middleware and not by clients! and, as an extra precaution,
880
891
  send a header named '\033[36mfinalmasterspark\033[0m' (a secret keyword)
881
892
  and then \033[36m--idp-h-key finalmasterspark\033[0m to require that
893
+
894
+ the login/logout links/buttons can be replaced with links
895
+ going to your IdP's UI; \033[36m--idp-login /login/?redir={dst}\033[0m
896
+ will expand \033[36m{dst}\033[0m to the URL of the current page, so
897
+ the IdP can redirect the user back to where they were
882
898
  """
883
899
  ),
884
900
  ],
@@ -1153,11 +1169,14 @@ def add_qr(ap, tty):
1153
1169
  ap2.add_argument("--qr-every", metavar="SEC", type=float, default=0, help="print the qr-code every \033[33mSEC\033[0m (try this with/without --qr-pin in case of issues)")
1154
1170
  ap2.add_argument("--qr-winch", metavar="SEC", type=float, default=0, help="when --qr-pin is enabled, check for terminal size change every \033[33mSEC\033[0m")
1155
1171
  ap2.add_argument("--qr-file", metavar="TXT", type=u, action="append", help="\033[34mREPEATABLE:\033[0m write qr-code to file.\n └─To create txt or svg, \033[33mTXT\033[0m is Filepath:Zoom:Pad, for example [\033[32mqr.txt:1:2\033[0m]\n └─To create png or gif, \033[33mTXT\033[0m is Filepath:Zoom:Pad:Foreground:Background, for example [\033[32mqr.png:8:2:333333:ffcc55\033[0m], or [\033[32mqr.png:8:2::ffcc55\033[0m] for transparent")
1172
+ ap2.add_argument("--qr-stdout", action="store_true", help="always display the QR-code on STDOUT in the terminal, even if \033[33m-q\033[0m")
1173
+ ap2.add_argument("--qr-stderr", action="store_true", help="always display the QR-code on STDERR in the terminal, even if \033[33m-q\033[0m")
1156
1174
 
1157
1175
 
1158
1176
  def add_fs(ap):
1159
1177
  ap2 = ap.add_argument_group("filesystem options")
1160
1178
  rm_re_def = "15/0.1" if ANYWIN else "0/0"
1179
+ ap2.add_argument("--casechk", metavar="N", type=u, default="auto", help="detect and prevent CI (case-insensitive) behavior if the underlying filesystem is CI? [\033[32my\033[0m] = detect and prevent, [\033[32mn\033[0m] = ignore and allow, [\033[32mauto\033[0m] = \033[32my\033[0m if CI fs detected. NOTE: \033[32my\033[0m is very slow but necessary for correct WebDAV behavior on Windows/Macos (volflag=casechk)")
1161
1180
  ap2.add_argument("--rm-retry", metavar="T/R", type=u, default=rm_re_def, help="if a file cannot be deleted because it is busy, continue trying for \033[33mT\033[0m seconds, retry every \033[33mR\033[0m seconds; disable with 0/0 (volflag=rm_retry)")
1162
1181
  ap2.add_argument("--mv-retry", metavar="T/R", type=u, default=rm_re_def, help="if a file cannot be renamed because it is busy, continue trying for \033[33mT\033[0m seconds, retry every \033[33mR\033[0m seconds; disable with 0/0 (volflag=mv_retry)")
1163
1182
  ap2.add_argument("--iobuf", metavar="BYTES", type=int, default=256*1024, help="file I/O buffer-size; if your volumes are on a network drive, try increasing to \033[32m524288\033[0m or even \033[32m4194304\033[0m (and let me know if that improves your performance)")
@@ -1169,6 +1188,7 @@ def add_share(ap):
1169
1188
  ap2 = ap.add_argument_group("share-url options")
1170
1189
  ap2.add_argument("--shr", metavar="DIR", type=u, default="", help="toplevel virtual folder for shared files/folders, for example [\033[32m/share\033[0m]")
1171
1190
  ap2.add_argument("--shr-db", metavar="FILE", type=u, default=db_path, help="database to store shares in")
1191
+ ap2.add_argument("--shr-who", metavar="TXT", type=u, default="auth", help="who can create a share? [\033[32mno\033[0m]=nobody, [\033[32ma\033[0m]=admin-permission, [\033[32mauth\033[0m]=authenticated (volflag=shr_who)")
1172
1192
  ap2.add_argument("--shr-adm", metavar="U,U", type=u, default="", help="comma-separated list of users allowed to view/delete any share")
1173
1193
  ap2.add_argument("--shr-rt", metavar="MIN", type=int, default=1440, help="shares can be revived by their owner if they expired less than MIN minutes ago; [\033[32m60\033[0m]=hour, [\033[32m1440\033[0m]=day, [\033[32m10080\033[0m]=week")
1174
1194
  ap2.add_argument("--shr-v", action="store_true", help="debug")
@@ -1285,6 +1305,9 @@ def add_auth(ap):
1285
1305
  ap2.add_argument("--idp-store", metavar="N", type=int, default=1, help="how to use \033[33m--idp-db\033[0m; [\033[32m0\033[0m] = entirely disable, [\033[32m1\033[0m] = write-only (effectively disabled), [\033[32m2\033[0m] = remember users, [\033[32m3\033[0m] = remember users and groups.\nNOTE: Will remember and restore the IdP-volumes of all users for all eternity if set to 2 or 3, even when user is deleted from your IdP")
1286
1306
  ap2.add_argument("--idp-adm", metavar="U,U", type=u, default="", help="comma-separated list of users allowed to use /?idp (the cache management UI)")
1287
1307
  ap2.add_argument("--idp-cookie", metavar="S", type=int, default=0, help="generate a session-token for IdP users which is written to cookie \033[33mcppws\033[0m (or \033[33mcppwd\033[0m if plaintext), to reduce the load on the IdP server, lifetime \033[33mS\033[0m seconds.\n └─note: The expiration time is a client hint only; the actual lifetime of the session-token is infinite (until next restart with \033[33m--ses-db\033[0m wiped)")
1308
+ ap2.add_argument("--idp-login", metavar="L", type=u, default="", help="replace all login-buttons with a link to URL \033[33mL\033[0m (unless \033[32mpw\033[0m is in \033[33m--auth-ord\033[0m then both will be shown); [\033[32m{dst}\033[0m] expands to url of current page")
1309
+ ap2.add_argument("--idp-login-t", metavar="T", type=u, default="Login with SSO", help="the label/text for the idp-login button")
1310
+ ap2.add_argument("--idp-logout", metavar="L", type=u, default="", help="replace all logout-buttons with a link to URL \033[33mL\033[0m")
1288
1311
  ap2.add_argument("--auth-ord", metavar="TXT", type=u, default="idp,ipu", help="controls auth precedence; examples: [\033[32mpw,idp,ipu\033[0m], [\033[32mipu,pw,idp\033[0m], see --help-auth-ord")
1289
1312
  ap2.add_argument("--no-bauth", action="store_true", help="disable basic-authentication support; do not accept passwords from the 'Authenticate' header at all. NOTE: This breaks support for the android app")
1290
1313
  ap2.add_argument("--bauth-last", action="store_true", help="keeps basic-authentication enabled, but only as a last-resort; if a cookie is also provided then the cookie wins")
@@ -1299,7 +1322,7 @@ def add_auth(ap):
1299
1322
  ap2.add_argument("--ao-idp-before-pw", type=u, default="", help=argparse.SUPPRESS)
1300
1323
  ap2.add_argument("--ao-h-before-hm", type=u, default="", help=argparse.SUPPRESS)
1301
1324
  ap2.add_argument("--ao-ipu-wins", type=u, default="", help=argparse.SUPPRESS)
1302
- ap2.add_argument("--ao-has-pw", type=u, default="", help=argparse.SUPPRESS)
1325
+ ap2.add_argument("--ao-have-pw", type=u, default="", help=argparse.SUPPRESS)
1303
1326
 
1304
1327
 
1305
1328
  def add_chpw(ap):
@@ -1445,6 +1468,7 @@ def add_yolo(ap):
1445
1468
  ap2.add_argument("--no-fnugg", action="store_true", help="disable the smoketest for caching-related issues in the web-UI")
1446
1469
  ap2.add_argument("--getmod", action="store_true", help="permit ?move=[...] and ?delete as GET")
1447
1470
  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)")
1471
+ ap2.add_argument("--unsafe-state", action="store_true", help="when one of the emergency fallback locations are used for runtime state ($TMPDIR, /tmp), certain features will be force-disabled for security reasons by default. This option overrides that safeguard and allows unsafe storage of secrets")
1448
1472
 
1449
1473
 
1450
1474
  def add_optouts(ap):
@@ -1458,7 +1482,7 @@ def add_optouts(ap):
1458
1482
  ap2.add_argument("--no-fs-abrt", action="store_true", help="disable ability to abort ongoing copy/move")
1459
1483
  ap2.add_argument("-nth", action="store_true", help="no title hostname; don't show \033[33m--name\033[0m in <title>")
1460
1484
  ap2.add_argument("-nih", action="store_true", help="no info hostname -- don't show in UI")
1461
- ap2.add_argument("-nid", action="store_true", help="no info disk-usage -- don't show in UI")
1485
+ ap2.add_argument("-nid", action="store_true", help="no info disk-usage -- don't show in UI. This is the same as --du-who no")
1462
1486
  ap2.add_argument("-nb", action="store_true", help="no powered-by-copyparty branding in UI")
1463
1487
  ap2.add_argument("--zipmaxn", metavar="N", type=u, default="0", help="reject download-as-zip if more than \033[33mN\033[0m files in total; optionally takes a unit suffix: [\033[32m256\033[0m], [\033[32m9K\033[0m], [\033[32m4G\033[0m] (volflag=zipmaxn)")
1464
1488
  ap2.add_argument("--zipmaxs", metavar="SZ", type=u, default="0", help="reject download-as-zip if total download size exceeds \033[33mSZ\033[0m bytes; optionally takes a unit suffix: [\033[32m256M\033[0m], [\033[32m4G\033[0m], [\033[32m2T\033[0m] (volflag=zipmaxs)")
@@ -1652,6 +1676,7 @@ def add_db_general(ap, hcores):
1652
1676
  ap2.add_argument("--hash-mt", metavar="CORES", type=int, default=hcores, help="num cpu cores to use for file hashing; set 0 or 1 for single-core hashing")
1653
1677
  ap2.add_argument("--re-maxage", metavar="SEC", type=int, default=0, help="rescan filesystem for changes every \033[33mSEC\033[0m seconds; 0=off (volflag=scan)")
1654
1678
  ap2.add_argument("--db-act", metavar="SEC", type=float, default=10.0, help="defer any scheduled volume reindexing until \033[33mSEC\033[0m seconds after last db write (uploads, renames, ...)")
1679
+ ap2.add_argument("--srch-icase", action="store_true", help="case-insensitive search for all unicode characters (the default is icase for just ascii). NOTE: will make searches much slower (around 4x), and NOTE: only applies to filenames/paths, not tags")
1655
1680
  ap2.add_argument("--srch-time", metavar="SEC", type=int, default=45, help="search deadline -- terminate searches running for more than \033[33mSEC\033[0m seconds")
1656
1681
  ap2.add_argument("--srch-hits", metavar="N", type=int, default=7999, help="max search results to allow clients to fetch; 125 results will be shown initially")
1657
1682
  ap2.add_argument("--srch-excl", metavar="PTN", type=u, default="", help="regex: exclude files from search results if the file-URL matches \033[33mPTN\033[0m (case-sensitive). Example: [\033[32mpassword|logs/[0-9]\033[0m] any URL containing 'password' or 'logs/DIGIT' (volflag=srch_excl)")
@@ -1706,7 +1731,7 @@ def add_og(ap):
1706
1731
  ap2.add_argument("--uqe", action="store_true", help="query-string parceling; translate a request for \033[33m/foo/.uqe/BASE64\033[0m into \033[33m/foo?TEXT\033[0m, or \033[33m/foo/?TEXT\033[0m if the first character in \033[33mTEXT\033[0m is a slash. Automatically enabled for \033[33m--og\033[0m")
1707
1732
 
1708
1733
 
1709
- def add_ui(ap, retry):
1734
+ def add_ui(ap, retry ):
1710
1735
  THEMES = 10
1711
1736
  ap2 = ap.add_argument_group("ui options")
1712
1737
  ap2.add_argument("--grid", action="store_true", help="show grid/thumbnails by default (volflag=grid)")
@@ -1736,7 +1761,11 @@ def add_ui(ap, retry):
1736
1761
  ap2.add_argument("--doctitle", metavar="TXT", type=u, default="copyparty @ --name", help="title / service-name to show in html documents")
1737
1762
  ap2.add_argument("--bname", metavar="TXT", type=u, default="--name", help="server name (displayed in filebrowser document title)")
1738
1763
  ap2.add_argument("--pb-url", metavar="URL", type=u, default=URL_PRJ, help="powered-by link; disable with \033[33m-nb\033[0m")
1739
- ap2.add_argument("--ver", action="store_true", help="show version on the control panel (incompatible with \033[33m-nb\033[0m)")
1764
+ ap2.add_argument("--ver", action="store_true", help="show version on the control panel (incompatible with \033[33m-nb\033[0m). This is the same as --ver-who all")
1765
+ ap2.add_argument("--ver-who", metavar="TXT", type=u, default="no", help="only show version for: [\033[32ma\033[0m]=admin-permission-anywhere, [\033[32mauth\033[0m]=authenticated, [\033[32mall\033[0m]=anyone")
1766
+ ap2.add_argument("--du-who", metavar="TXT", type=u, default="all", help="only show disk usage for: [\033[32mno\033[0m]=nobody, [\033[32ma\033[0m]=admin-permission, [\033[32mrw\033[0m]=read-write, [\033[32mw\033[0m]=write, [\033[32mauth\033[0m]=authenticated, [\033[32mall\033[0m]=anyone (volflag=du_who)")
1767
+ ap2.add_argument("--ver-iwho", type=int, default=0, help=argparse.SUPPRESS)
1768
+ ap2.add_argument("--du-iwho", type=int, default=0, help=argparse.SUPPRESS)
1740
1769
  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")
1741
1770
  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")
1742
1771
  ap2.add_argument("--ctl-re", metavar="SEC", type=int, default=1, help="the controlpanel Refresh-button will autorefresh every SEC; [\033[32m0\033[0m] = just once")
@@ -1844,18 +1873,21 @@ def run_argparse(
1844
1873
  for k, h, _ in sects:
1845
1874
  ap2.add_argument("--help-" + k, action="store_true", help=h)
1846
1875
 
1847
- try:
1848
- if not retry:
1849
- raise Exception()
1850
-
1876
+ if retry:
1877
+ a = ["ascii", "replace"]
1851
1878
  for x in ap._actions:
1852
- if not x.help:
1853
- continue
1879
+ try:
1880
+ x.default = x.default.encode(*a).decode(*a)
1881
+ except:
1882
+ pass
1854
1883
 
1855
- a = ["ascii", "replace"]
1856
- x.help = x.help.encode(*a).decode(*a) + "\033[0m"
1857
- except:
1858
- pass
1884
+ try:
1885
+ if x.help and x.help is not argparse.SUPPRESS:
1886
+ x.help = x.help.replace("└─", "`-").encode(*a).decode(*a)
1887
+ if retry > 2:
1888
+ x.help = RE_ANSI.sub("", x.help)
1889
+ except:
1890
+ pass
1859
1891
 
1860
1892
  ret = ap.parse_args(args=argv[1:])
1861
1893
  for k, h, t in sects:
@@ -1965,7 +1997,7 @@ def main(argv = None) :
1965
1997
  except:
1966
1998
  nc = 486 # mdns/ssdp restart headroom; select() maxfd is 512 on windows
1967
1999
 
1968
- retry = False
2000
+ retry = 0
1969
2001
  for fmtr in [RiceFormatter, RiceFormatter, Dodge11874, BasicDodge11874]:
1970
2002
  try:
1971
2003
  al = run_argparse(argv, fmtr, retry, nc)
@@ -1974,8 +2006,9 @@ def main(argv = None) :
1974
2006
  except SystemExit:
1975
2007
  raise
1976
2008
  except:
1977
- retry = True
1978
- lprint("\n[ {} ]:\n{}\n".format(fmtr, min_ex()))
2009
+ retry += 1
2010
+ t = "WARNING: due to limitations in your terminal and/or OS, the helptext cannot be displayed correctly. Will show a simplified version due to the following error:\n[ %s ]:\n%s\n"
2011
+ lprint(t % (fmtr, min_ex()))
1979
2012
 
1980
2013
  try:
1981
2014
  assert al # type: ignore
copyparty/__version__.py CHANGED
@@ -1,8 +1,8 @@
1
1
  # coding: utf-8
2
2
 
3
- VERSION = (1, 19, 7)
3
+ VERSION = (1, 19, 9)
4
4
  CODENAME = "usernames"
5
- BUILD_DT = (2025, 8, 28)
5
+ BUILD_DT = (2025, 9, 15)
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
@@ -13,7 +13,7 @@ import threading
13
13
  import time
14
14
  from datetime import datetime
15
15
 
16
- from .__init__ import ANYWIN, PY2, TYPE_CHECKING, WINDOWS, E
16
+ from .__init__ import ANYWIN, MACOS, PY2, TYPE_CHECKING, WINDOWS, E
17
17
  from .bos import bos
18
18
  from .cfg import flagdescs, permdescs, vf_bmap, vf_cmap, vf_vmap
19
19
  from .pwhash import PWHash
@@ -92,6 +92,8 @@ SBADCFG = " ({})".format(BAD_CFG)
92
92
 
93
93
  PTN_U_GRP = re.compile(r"\$\{u(%[+-][^}]+)\}")
94
94
  PTN_G_GRP = re.compile(r"\$\{g(%[+-][^}]+)\}")
95
+ PTN_U_ANY = re.compile(r"(\${[u][}%])")
96
+ PTN_G_ANY = re.compile(r"(\${[g][}%])")
95
97
  PTN_SIGIL = re.compile(r"(\${[ug][}%])")
96
98
 
97
99
 
@@ -417,10 +419,14 @@ class VFS(object):
417
419
  self.all_nodes[vpath] = self
418
420
  self.all_aps = [(rp, [self])]
419
421
  self.all_vps = [(vp, self)]
422
+ self.canonical = self._canonical
423
+ self.dcanonical = self._dcanonical
420
424
  else:
421
425
  self.histpath = self.dbpath = ""
422
426
  self.all_aps = []
423
427
  self.all_vps = []
428
+ self.canonical = self._canonical_null
429
+ self.dcanonical = self._dcanonical_null
424
430
 
425
431
  self.get_dbv = self._get_dbv
426
432
  self.ls = self._ls
@@ -617,7 +623,35 @@ class VFS(object):
617
623
  vrem = vjoin(self.vpath[len(dbv.vpath) :].lstrip("/"), vrem)
618
624
  return dbv, vrem
619
625
 
620
- def canonical(self, rem , resolve = True) :
626
+ def casechk(self, rem , do_stat ) :
627
+ ap = self.canonical(rem, False)
628
+ if do_stat and not bos.path.exists(ap):
629
+ return True # doesn't exist at all; good to go
630
+ dp, fn = os.path.split(ap)
631
+ try:
632
+ fns = os.listdir(dp)
633
+ except:
634
+ return True # maybe chmod 111; assume ok
635
+ if fn in fns:
636
+ return True
637
+ hit = "<?>"
638
+ lfn = fn.lower()
639
+ for zs in fns:
640
+ if lfn == zs.lower():
641
+ hit = zs
642
+ break
643
+ if self.log:
644
+ t = "returning 404 due to underlying case-insensitive filesystem:\n http-req: %r\n local-fs: %r"
645
+ self.log("vfs", t % (fn, hit))
646
+ return False
647
+
648
+ def _canonical_null(self, rem , resolve = True) :
649
+ return ""
650
+
651
+ def _dcanonical_null(self, rem ) :
652
+ return ""
653
+
654
+ def _canonical(self, rem , resolve = True) :
621
655
  """returns the canonical path (fully-resolved absolute fs path)"""
622
656
  ap = self.realpath
623
657
  if rem:
@@ -625,7 +659,7 @@ class VFS(object):
625
659
 
626
660
  return absreal(ap) if resolve else ap
627
661
 
628
- def dcanonical(self, rem ) :
662
+ def _dcanonical(self, rem ) :
629
663
  """resolves until the final component (filename)"""
630
664
  ap = self.realpath
631
665
  if rem:
@@ -634,6 +668,42 @@ class VFS(object):
634
668
  ad, fn = os.path.split(ap)
635
669
  return os.path.join(absreal(ad), fn)
636
670
 
671
+ def _canonical_shr(self, rem , resolve = True) :
672
+ """returns the canonical path (fully-resolved absolute fs path)"""
673
+ ap = self.realpath
674
+ if rem:
675
+ ap += "/" + rem
676
+
677
+ rap = absreal(ap)
678
+ if self.shr_files:
679
+ vn, rem = self.shr_src
680
+ chk = absreal(os.path.join(vn.realpath, rem))
681
+ if chk != rap:
682
+ # not the dir itself; assert file allowed
683
+ ad, fn = os.path.split(rap)
684
+ if chk != ad or fn not in self.shr_files:
685
+ return "\n\n"
686
+
687
+ return rap if resolve else ap
688
+
689
+ def _dcanonical_shr(self, rem ) :
690
+ """resolves until the final component (filename)"""
691
+ ap = self.realpath
692
+ if rem:
693
+ ap += "/" + rem
694
+
695
+ ad, fn = os.path.split(ap)
696
+ ad = absreal(ad)
697
+ if self.shr_files:
698
+ vn, rem = self.shr_src
699
+ chk = absreal(os.path.join(vn.realpath, rem))
700
+ if chk != absreal(ap):
701
+ # not the dir itself; assert file allowed
702
+ if ad != chk or fn not in self.shr_files:
703
+ return "\n\n"
704
+
705
+ return os.path.join(ad, fn)
706
+
637
707
  def _ls_nope(
638
708
  self, *a, **ka
639
709
  ) :
@@ -666,8 +736,12 @@ class VFS(object):
666
736
  """return user-readable [fsdir,real,virt] items at vpath"""
667
737
  virt_vis = {} # nodes readable by user
668
738
  abspath = self.canonical(rem)
669
- real = list(statdir(self.log, scandir, lstat, abspath, throw))
670
- real.sort()
739
+ if abspath:
740
+ real = list(statdir(self.log, scandir, lstat, abspath, throw))
741
+ real.sort()
742
+ else:
743
+ real = []
744
+
671
745
  if not rem:
672
746
  # no vfs nodes in the list of real inodes
673
747
  real = [x for x in real if x[0] not in self.nodes]
@@ -969,6 +1043,14 @@ class AuthSrv(object):
969
1043
  self.indent = ""
970
1044
  self.is_lxc = args.c == ["/z/initcfg"]
971
1045
 
1046
+ self._vf0b = {
1047
+ "tcolor": self.args.tcolor,
1048
+ "du_iwho": self.args.du_iwho,
1049
+ "shr_who": self.args.shr_who if self.args.shr else "no",
1050
+ }
1051
+ self._vf0 = self._vf0b.copy()
1052
+ self._vf0["d2d"] = True
1053
+
972
1054
  # fwd-decl
973
1055
  self.vfs = VFS(log_func, "", "", "", AXS(), {})
974
1056
  self.acct = {} # uname->pw
@@ -1007,7 +1089,10 @@ class AuthSrv(object):
1007
1089
  yield prev, True
1008
1090
 
1009
1091
  def vf0(self):
1010
- return {"d2d": True, "tcolor": self.args.tcolor}
1092
+ return self._vf0.copy()
1093
+
1094
+ def vf0b(self):
1095
+ return self._vf0b.copy()
1011
1096
 
1012
1097
  def idp_checkin(
1013
1098
  self, broker , uname , gname
@@ -1072,6 +1157,16 @@ class AuthSrv(object):
1072
1157
  src0 = src # abspath
1073
1158
  dst0 = dst # vpath
1074
1159
 
1160
+ zsl = []
1161
+ for ptn, sigil in ((PTN_U_ANY, "${u}"), (PTN_G_ANY, "${g}")):
1162
+ if bool(ptn.search(src)) != bool(ptn.search(dst)):
1163
+ zsl.append(sigil)
1164
+ if zsl:
1165
+ t = "ERROR: if %s is mentioned in a volume definition, it must be included in both the filesystem-path [%s] and the volume-url [/%s]"
1166
+ t = "\n".join([t % (x, src, dst) for x in zsl])
1167
+ self.log(t, 1)
1168
+ raise Exception(t)
1169
+
1075
1170
  un_gn = [(un, gn) for un, gns in un_gns.items() for gn in gns]
1076
1171
  if not un_gn:
1077
1172
  # ensure volume creation if there's no users
@@ -1164,8 +1259,8 @@ class AuthSrv(object):
1164
1259
  self.log(t, c=3)
1165
1260
  raise Exception(BAD_CFG)
1166
1261
 
1167
- if not bos.path.isdir(src):
1168
- self.log("warning: filesystem-path does not exist: {}".format(src), 3)
1262
+ if not bos.path.exists(src):
1263
+ self.log("warning: filesystem-path did not exist: %r" % (src,), 3)
1169
1264
 
1170
1265
  mount[dst] = (src, dst0)
1171
1266
  daxs[dst] = AXS()
@@ -1743,12 +1838,15 @@ class AuthSrv(object):
1743
1838
  files = os.listdir(E.cfg)
1744
1839
  except:
1745
1840
  files = []
1746
- hits = [x for x in files if x.lower().endswith(".conf")]
1841
+ hits = [
1842
+ x
1843
+ for x in files
1844
+ if x.lower().endswith(".conf") and not x.startswith(".")
1845
+ ]
1747
1846
  if hits:
1748
1847
  t = "Hint: Found some config files in [%s], but these were not automatically loaded because they are in the wrong place%s %s\n"
1749
1848
  self.log(t % (E.cfg, ehint, ", ".join(hits)), 3)
1750
- zvf = {"tcolor": self.args.tcolor}
1751
- vfs = VFS(self.log_func, absreal("."), "", "", axs, zvf)
1849
+ vfs = VFS(self.log_func, absreal("."), "", "", axs, self.vf0b())
1752
1850
  if not axs.uread:
1753
1851
  self.badcfg1 = True
1754
1852
  elif "" not in mount:
@@ -1784,7 +1882,7 @@ class AuthSrv(object):
1784
1882
  vol.all_vps.sort(key=lambda x: len(x[0]), reverse=True)
1785
1883
  vol.root = vfs
1786
1884
 
1787
- zs = "neversymlink"
1885
+ zs = "neversymlink du_iwho"
1788
1886
  k_ign = set(zs.split())
1789
1887
  for vol in vfs.all_vols.values():
1790
1888
  unknown_flags = set()
@@ -1935,6 +2033,8 @@ class AuthSrv(object):
1935
2033
  promote = []
1936
2034
  demote = []
1937
2035
  for vol in vfs.all_vols.values():
2036
+ if not vol.realpath:
2037
+ continue
1938
2038
  hid = self.hid_cache.get(vol.realpath)
1939
2039
  if not hid:
1940
2040
  zb = hashlib.sha512(afsenc(vol.realpath)).digest()
@@ -1973,6 +2073,8 @@ class AuthSrv(object):
1973
2073
  vol.histpath = absreal(vol.histpath)
1974
2074
 
1975
2075
  for vol in vfs.all_vols.values():
2076
+ if not vol.realpath:
2077
+ continue
1976
2078
  hid = self.hid_cache[vol.realpath]
1977
2079
  vflag = vol.flags.get("dbpath")
1978
2080
  if vflag == "-":
@@ -2281,6 +2383,11 @@ class AuthSrv(object):
2281
2383
  vol.lim.uid = vol.flags["uid"]
2282
2384
  vol.lim.gid = vol.flags["gid"]
2283
2385
 
2386
+ vol.flags["du_iwho"] = n_du_who(vol.flags["du_who"])
2387
+
2388
+ if not enshare:
2389
+ vol.flags["shr_who"] = self.args.shr_who = "no"
2390
+
2284
2391
  if vol.flags.get("og"):
2285
2392
  self.args.uqe = True
2286
2393
 
@@ -2456,6 +2563,47 @@ class AuthSrv(object):
2456
2563
  self.log(t.format(vol.vpath, mtp), 1)
2457
2564
  errors = True
2458
2565
 
2566
+ for vol in vfs.all_nodes.values():
2567
+ if not vol.realpath or os.path.isfile(vol.realpath):
2568
+ continue
2569
+ ccs = vol.flags["casechk"][:1].lower()
2570
+ if ccs in ("y", "n"):
2571
+ if ccs == "y":
2572
+ vol.flags["bcasechk"] = True
2573
+ continue
2574
+ try:
2575
+ bos.makedirs(vol.realpath, vf=vol.flags)
2576
+ files = os.listdir(vol.realpath)
2577
+ for fn in files:
2578
+ fn2 = fn.lower()
2579
+ if fn == fn2:
2580
+ fn2 = fn.upper()
2581
+ if fn == fn2 or fn2 in files:
2582
+ continue
2583
+ is_ci = os.path.exists(os.path.join(vol.realpath, fn2))
2584
+ ccs = "y" if is_ci else "n"
2585
+ break
2586
+ if ccs not in ("y", "n"):
2587
+ ap = os.path.join(vol.realpath, "casechk")
2588
+ open(ap, "wb").close()
2589
+ ccs = "y" if os.path.exists(ap[:-1] + "K") else "n"
2590
+ os.unlink(ap)
2591
+ except Exception as ex:
2592
+ if ANYWIN:
2593
+ zs = "Windows"
2594
+ ccs = "y"
2595
+ elif MACOS:
2596
+ zs = "Macos"
2597
+ ccs = "y"
2598
+ else:
2599
+ zs = "Linux"
2600
+ ccs = "n"
2601
+ t = "unable to determine if filesystem at %r is case-insensitive due to %r; assuming casechk=%s due to %s"
2602
+ self.log(t % (vol.realpath, ex, ccs, zs), 3)
2603
+ vol.flags["casechk"] = ccs
2604
+ if ccs == "y":
2605
+ vol.flags["bcasechk"] = True
2606
+
2459
2607
  tags = self.args.mtp or []
2460
2608
  tags = [x.split("=")[0] for x in tags]
2461
2609
  tags = [y for x in tags for y in x.split(",")]
@@ -2723,8 +2871,12 @@ class AuthSrv(object):
2723
2871
 
2724
2872
  shn.shr_files = set(fns)
2725
2873
  shn.ls = shn._ls_shr
2874
+ shn.canonical = shn._canonical_shr
2875
+ shn.dcanonical = shn._dcanonical_shr
2726
2876
  else:
2727
2877
  shn.ls = shn._ls
2878
+ shn.canonical = shn._canonical
2879
+ shn.dcanonical = shn._dcanonical
2728
2880
 
2729
2881
  shn.shr_owner = s_un
2730
2882
  shn.shr_src = (s_vfs, s_rem)
@@ -2787,6 +2939,7 @@ class AuthSrv(object):
2787
2939
  "dcrop": vf["crop"],
2788
2940
  "dth3x": vf["th3x"],
2789
2941
  "u2ts": vf["u2ts"],
2942
+ "shr_who": vf["shr_who"],
2790
2943
  "frand": bool(vf.get("rand")),
2791
2944
  "lifetime": vf.get("lifetime") or 0,
2792
2945
  "unlist": vf.get("unlist") or "",
@@ -2795,11 +2948,13 @@ class AuthSrv(object):
2795
2948
  js_htm = {
2796
2949
  "SPINNER": self.args.spinner,
2797
2950
  "s_name": self.args.bname,
2951
+ "idp_login": self.args.idp_login,
2798
2952
  "have_up2k_idx": "e2d" in vf,
2799
2953
  "have_acode": not self.args.no_acode,
2800
2954
  "have_c2flac": self.args.allow_flac,
2801
2955
  "have_c2wav": self.args.allow_wav,
2802
2956
  "have_shr": self.args.shr,
2957
+ "shr_who": vf["shr_who"],
2803
2958
  "have_zip": not self.args.no_zip,
2804
2959
  "have_mv": not self.args.no_mv,
2805
2960
  "have_del": not self.args.no_del,
@@ -2865,7 +3020,7 @@ class AuthSrv(object):
2865
3020
  self.args.ao_idp_before_pw = min(h, hm) < pw
2866
3021
  self.args.ao_h_before_hm = h < hm
2867
3022
  self.args.ao_ipu_wins = ipu == 0
2868
- self.args.ao_have_pw = pw < 99
3023
+ self.args.ao_have_pw = pw < 99 or not self.args.have_idp_hdrs
2869
3024
 
2870
3025
  def load_idp_db(self, quiet=False) :
2871
3026
  # mutex me
@@ -3456,6 +3611,30 @@ class AuthSrv(object):
3456
3611
  self.log("generated config:\n\n" + "\n".join(ret))
3457
3612
 
3458
3613
 
3614
+ def n_du_who(s ) :
3615
+ if s == "all":
3616
+ return 9
3617
+ if s == "auth":
3618
+ return 7
3619
+ if s == "w":
3620
+ return 5
3621
+ if s == "rw":
3622
+ return 4
3623
+ if s == "a":
3624
+ return 3
3625
+ return 0
3626
+
3627
+
3628
+ def n_ver_who(s ) :
3629
+ if s == "all":
3630
+ return 9
3631
+ if s == "auth":
3632
+ return 6
3633
+ if s == "a":
3634
+ return 3
3635
+ return 0
3636
+
3637
+
3459
3638
  def split_cfg_ln(ln ) :
3460
3639
  # "a, b, c: 3" => {a:true, b:true, c:3}
3461
3640
  ret = {}
@@ -3488,7 +3667,9 @@ def expand_config_file(
3488
3667
 
3489
3668
  if os.path.isdir(fp):
3490
3669
  names = list(sorted(os.listdir(fp)))
3491
- cnames = [x for x in names if x.lower().endswith(".conf")]
3670
+ cnames = [
3671
+ x for x in names if x.lower().endswith(".conf") and not x.startswith(".")
3672
+ ]
3492
3673
  if not cnames:
3493
3674
  t = "warning: tried to read config-files from folder '%s' but it does not contain any "
3494
3675
  if names: