copyparty 1.19.8__tar.gz → 1.19.9__tar.gz

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 (124) hide show
  1. {copyparty-1.19.8 → copyparty-1.19.9}/PKG-INFO +5 -1
  2. {copyparty-1.19.8 → copyparty-1.19.9}/README.md +4 -0
  3. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/__main__.py +22 -14
  4. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/__version__.py +2 -2
  5. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/authsrv.py +102 -9
  6. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/cfg.py +2 -0
  7. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/ftpd.py +3 -0
  8. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/httpcli.py +23 -11
  9. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/mdns.py +3 -1
  10. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/mtag.py +0 -1
  11. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/svchub.py +11 -2
  12. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/u2idx.py +29 -4
  13. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/up2k.py +15 -5
  14. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/util.py +41 -19
  15. copyparty-1.19.9/copyparty/web/baguettebox.js.gz +0 -0
  16. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/web/browser.css.gz +0 -0
  17. copyparty-1.19.9/copyparty/web/browser.js.gz +0 -0
  18. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/web/dbg-audio.js.gz +0 -0
  19. copyparty-1.19.9/copyparty/web/deps/busy.mp3.gz +0 -0
  20. copyparty-1.19.9/copyparty/web/deps/easymde.css.gz +0 -0
  21. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/web/deps/easymde.js.gz +0 -0
  22. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/web/deps/marked.js.gz +0 -0
  23. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/web/deps/mini-fa.css.gz +0 -0
  24. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/web/deps/prism.css.gz +0 -0
  25. copyparty-1.19.9/copyparty/web/deps/prism.js.gz +0 -0
  26. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/web/deps/prismd.css.gz +0 -0
  27. copyparty-1.19.9/copyparty/web/deps/scp.woff2 +0 -0
  28. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/web/deps/sha512.ac.js.gz +0 -0
  29. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/web/md.css.gz +0 -0
  30. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/web/md.js.gz +0 -0
  31. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/web/md2.css.gz +0 -0
  32. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/web/md2.js.gz +0 -0
  33. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/web/mde.css.gz +0 -0
  34. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/web/mde.js.gz +0 -0
  35. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/web/msg.css.gz +0 -0
  36. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/web/rups.css.gz +0 -0
  37. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/web/rups.js.gz +0 -0
  38. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/web/shares.css.gz +0 -0
  39. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/web/shares.js.gz +0 -0
  40. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/web/splash.css.gz +0 -0
  41. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/web/splash.js.gz +0 -0
  42. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/web/svcs.js.gz +0 -0
  43. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/web/ui.css.gz +0 -0
  44. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/web/up2k.js.gz +0 -0
  45. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/web/util.js.gz +0 -0
  46. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/web/w.hash.js.gz +0 -0
  47. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty.egg-info/PKG-INFO +5 -1
  48. copyparty-1.19.8/copyparty/web/baguettebox.js.gz +0 -0
  49. copyparty-1.19.8/copyparty/web/browser.js.gz +0 -0
  50. copyparty-1.19.8/copyparty/web/deps/busy.mp3.gz +0 -0
  51. copyparty-1.19.8/copyparty/web/deps/easymde.css.gz +0 -0
  52. copyparty-1.19.8/copyparty/web/deps/prism.js.gz +0 -0
  53. copyparty-1.19.8/copyparty/web/deps/scp.woff2 +0 -0
  54. {copyparty-1.19.8 → copyparty-1.19.9}/LICENSE +0 -0
  55. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/__init__.py +0 -0
  56. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/bos/__init__.py +0 -0
  57. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/bos/bos.py +0 -0
  58. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/bos/path.py +0 -0
  59. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/broker_mp.py +0 -0
  60. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/broker_mpw.py +0 -0
  61. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/broker_thr.py +0 -0
  62. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/broker_util.py +0 -0
  63. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/cert.py +0 -0
  64. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/dxml.py +0 -0
  65. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/fsutil.py +0 -0
  66. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/httpconn.py +0 -0
  67. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/httpsrv.py +0 -0
  68. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/ico.py +0 -0
  69. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/metrics.py +0 -0
  70. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/multicast.py +0 -0
  71. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/pwhash.py +0 -0
  72. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/res/COPYING.txt +0 -0
  73. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/res/__init__.py +0 -0
  74. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/res/insecure.pem +0 -0
  75. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/smbd.py +0 -0
  76. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/ssdp.py +0 -0
  77. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/star.py +0 -0
  78. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/stolen/__init__.py +0 -0
  79. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/stolen/dnslib/__init__.py +0 -0
  80. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/stolen/dnslib/bimap.py +0 -0
  81. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/stolen/dnslib/bit.py +0 -0
  82. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/stolen/dnslib/buffer.py +0 -0
  83. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/stolen/dnslib/dns.py +0 -0
  84. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/stolen/dnslib/label.py +0 -0
  85. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/stolen/dnslib/lex.py +0 -0
  86. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/stolen/dnslib/ranges.py +0 -0
  87. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/stolen/ifaddr/__init__.py +0 -0
  88. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/stolen/ifaddr/_posix.py +0 -0
  89. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/stolen/ifaddr/_shared.py +0 -0
  90. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/stolen/ifaddr/_win32.py +0 -0
  91. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/stolen/qrcodegen.py +0 -0
  92. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/stolen/surrogateescape.py +0 -0
  93. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/sutil.py +0 -0
  94. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/szip.py +0 -0
  95. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/tcpsrv.py +0 -0
  96. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/tftpd.py +0 -0
  97. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/th_cli.py +0 -0
  98. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/th_srv.py +0 -0
  99. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/web/a/__init__.py +0 -0
  100. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/web/a/partyfuse.py +0 -0
  101. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/web/a/u2c.py +0 -0
  102. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/web/a/webdav-cfg.bat +0 -0
  103. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/web/browser.html +0 -0
  104. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/web/browser2.html +0 -0
  105. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/web/cf.html +0 -0
  106. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/web/deps/__init__.py +0 -0
  107. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/web/deps/fuse.py +0 -0
  108. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/web/deps/mini-fa.woff +0 -0
  109. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/web/deps/sha512.hw.js.gz +0 -0
  110. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/web/idp.html +0 -0
  111. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/web/md.html +0 -0
  112. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/web/mde.html +0 -0
  113. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/web/msg.html +0 -0
  114. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/web/rups.html +0 -0
  115. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/web/shares.html +0 -0
  116. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/web/splash.html +0 -0
  117. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty/web/svcs.html +0 -0
  118. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty.egg-info/SOURCES.txt +0 -0
  119. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty.egg-info/dependency_links.txt +0 -0
  120. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty.egg-info/entry_points.txt +0 -0
  121. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty.egg-info/requires.txt +0 -0
  122. {copyparty-1.19.8 → copyparty-1.19.9}/copyparty.egg-info/top_level.txt +0 -0
  123. {copyparty-1.19.8 → copyparty-1.19.9}/pyproject.toml +0 -0
  124. {copyparty-1.19.8 → copyparty-1.19.9}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: copyparty
3
- Version: 1.19.8
3
+ Version: 1.19.9
4
4
  Summary: Portable file server with accelerated resumable uploads, deduplication, WebDAV, FTP, zeroconf, media indexer, video thumbnails, audio transcoding, and write-only folders
5
5
  Author-email: ed <copyparty@ocv.me>
6
6
  License: MIT
@@ -2770,6 +2770,10 @@ below are some tweaks roughly ordered by usefulness:
2770
2770
  * using [pypy](https://www.pypy.org/) instead of [cpython](https://www.python.org/) *can* be 70% faster for some workloads, but slower for many others
2771
2771
  * and pypy can sometimes crash on startup with `-j0` (TODO make issue)
2772
2772
 
2773
+ * if you are running the copyparty server **on Windows or Macos:**
2774
+ * `--casechk=y` makes it much faster, but also awakens [the usual surprises](https://github.com/9001/copyparty/issues/781) you expect from a case-insensitive filesystem
2775
+ * this is the same as `casechk: n` in a config-file
2776
+
2773
2777
 
2774
2778
  ## client-side
2775
2779
 
@@ -2705,6 +2705,10 @@ below are some tweaks roughly ordered by usefulness:
2705
2705
  * using [pypy](https://www.pypy.org/) instead of [cpython](https://www.python.org/) *can* be 70% faster for some workloads, but slower for many others
2706
2706
  * and pypy can sometimes crash on startup with `-j0` (TODO make issue)
2707
2707
 
2708
+ * if you are running the copyparty server **on Windows or Macos:**
2709
+ * `--casechk=y` makes it much faster, but also awakens [the usual surprises](https://github.com/9001/copyparty/issues/781) you expect from a case-insensitive filesystem
2710
+ * this is the same as `casechk: n` in a config-file
2711
+
2708
2712
 
2709
2713
  ## client-side
2710
2714
 
@@ -1169,11 +1169,14 @@ def add_qr(ap, tty):
1169
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)")
1170
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")
1171
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")
1172
1174
 
1173
1175
 
1174
1176
  def add_fs(ap):
1175
1177
  ap2 = ap.add_argument_group("filesystem options")
1176
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)")
1177
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)")
1178
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)")
1179
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)")
@@ -1673,6 +1676,7 @@ def add_db_general(ap, hcores):
1673
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")
1674
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)")
1675
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")
1676
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")
1677
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")
1678
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)")
@@ -1727,7 +1731,7 @@ def add_og(ap):
1727
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")
1728
1732
 
1729
1733
 
1730
- def add_ui(ap, retry):
1734
+ def add_ui(ap, retry ):
1731
1735
  THEMES = 10
1732
1736
  ap2 = ap.add_argument_group("ui options")
1733
1737
  ap2.add_argument("--grid", action="store_true", help="show grid/thumbnails by default (volflag=grid)")
@@ -1869,18 +1873,21 @@ def run_argparse(
1869
1873
  for k, h, _ in sects:
1870
1874
  ap2.add_argument("--help-" + k, action="store_true", help=h)
1871
1875
 
1872
- try:
1873
- if not retry:
1874
- raise Exception()
1875
-
1876
+ if retry:
1877
+ a = ["ascii", "replace"]
1876
1878
  for x in ap._actions:
1877
- if not x.help:
1878
- continue
1879
+ try:
1880
+ x.default = x.default.encode(*a).decode(*a)
1881
+ except:
1882
+ pass
1879
1883
 
1880
- a = ["ascii", "replace"]
1881
- x.help = x.help.encode(*a).decode(*a) + "\033[0m"
1882
- except:
1883
- 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
1884
1891
 
1885
1892
  ret = ap.parse_args(args=argv[1:])
1886
1893
  for k, h, t in sects:
@@ -1990,7 +1997,7 @@ def main(argv = None) :
1990
1997
  except:
1991
1998
  nc = 486 # mdns/ssdp restart headroom; select() maxfd is 512 on windows
1992
1999
 
1993
- retry = False
2000
+ retry = 0
1994
2001
  for fmtr in [RiceFormatter, RiceFormatter, Dodge11874, BasicDodge11874]:
1995
2002
  try:
1996
2003
  al = run_argparse(argv, fmtr, retry, nc)
@@ -1999,8 +2006,9 @@ def main(argv = None) :
1999
2006
  except SystemExit:
2000
2007
  raise
2001
2008
  except:
2002
- retry = True
2003
- 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()))
2004
2012
 
2005
2013
  try:
2006
2014
  assert al # type: ignore
@@ -1,8 +1,8 @@
1
1
  # coding: utf-8
2
2
 
3
- VERSION = (1, 19, 8)
3
+ VERSION = (1, 19, 9)
4
4
  CODENAME = "usernames"
5
- BUILD_DT = (2025, 9, 7)
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)
@@ -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,15 +419,17 @@ 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
427
- self.canonical = self._canonical
428
- self.dcanonical = self._dcanonical
429
433
 
430
434
  def __repr__(self) :
431
435
  return "VFS(%s)" % (
@@ -619,6 +623,34 @@ class VFS(object):
619
623
  vrem = vjoin(self.vpath[len(dbv.vpath) :].lstrip("/"), vrem)
620
624
  return dbv, vrem
621
625
 
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
+
622
654
  def _canonical(self, rem , resolve = True) :
623
655
  """returns the canonical path (fully-resolved absolute fs path)"""
624
656
  ap = self.realpath
@@ -704,8 +736,12 @@ class VFS(object):
704
736
  """return user-readable [fsdir,real,virt] items at vpath"""
705
737
  virt_vis = {} # nodes readable by user
706
738
  abspath = self.canonical(rem)
707
- real = list(statdir(self.log, scandir, lstat, abspath, throw))
708
- real.sort()
739
+ if abspath:
740
+ real = list(statdir(self.log, scandir, lstat, abspath, throw))
741
+ real.sort()
742
+ else:
743
+ real = []
744
+
709
745
  if not rem:
710
746
  # no vfs nodes in the list of real inodes
711
747
  real = [x for x in real if x[0] not in self.nodes]
@@ -1121,6 +1157,16 @@ class AuthSrv(object):
1121
1157
  src0 = src # abspath
1122
1158
  dst0 = dst # vpath
1123
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
+
1124
1170
  un_gn = [(un, gn) for un, gns in un_gns.items() for gn in gns]
1125
1171
  if not un_gn:
1126
1172
  # ensure volume creation if there's no users
@@ -1213,8 +1259,8 @@ class AuthSrv(object):
1213
1259
  self.log(t, c=3)
1214
1260
  raise Exception(BAD_CFG)
1215
1261
 
1216
- if not bos.path.isdir(src):
1217
- 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)
1218
1264
 
1219
1265
  mount[dst] = (src, dst0)
1220
1266
  daxs[dst] = AXS()
@@ -1836,7 +1882,7 @@ class AuthSrv(object):
1836
1882
  vol.all_vps.sort(key=lambda x: len(x[0]), reverse=True)
1837
1883
  vol.root = vfs
1838
1884
 
1839
- zs = "neversymlink"
1885
+ zs = "neversymlink du_iwho"
1840
1886
  k_ign = set(zs.split())
1841
1887
  for vol in vfs.all_vols.values():
1842
1888
  unknown_flags = set()
@@ -1987,6 +2033,8 @@ class AuthSrv(object):
1987
2033
  promote = []
1988
2034
  demote = []
1989
2035
  for vol in vfs.all_vols.values():
2036
+ if not vol.realpath:
2037
+ continue
1990
2038
  hid = self.hid_cache.get(vol.realpath)
1991
2039
  if not hid:
1992
2040
  zb = hashlib.sha512(afsenc(vol.realpath)).digest()
@@ -2025,6 +2073,8 @@ class AuthSrv(object):
2025
2073
  vol.histpath = absreal(vol.histpath)
2026
2074
 
2027
2075
  for vol in vfs.all_vols.values():
2076
+ if not vol.realpath:
2077
+ continue
2028
2078
  hid = self.hid_cache[vol.realpath]
2029
2079
  vflag = vol.flags.get("dbpath")
2030
2080
  if vflag == "-":
@@ -2336,7 +2386,7 @@ class AuthSrv(object):
2336
2386
  vol.flags["du_iwho"] = n_du_who(vol.flags["du_who"])
2337
2387
 
2338
2388
  if not enshare:
2339
- vol.flags["shr_who"] = "no"
2389
+ vol.flags["shr_who"] = self.args.shr_who = "no"
2340
2390
 
2341
2391
  if vol.flags.get("og"):
2342
2392
  self.args.uqe = True
@@ -2513,6 +2563,47 @@ class AuthSrv(object):
2513
2563
  self.log(t.format(vol.vpath, mtp), 1)
2514
2564
  errors = True
2515
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
+
2516
2607
  tags = self.args.mtp or []
2517
2608
  tags = [x.split("=")[0] for x in tags]
2518
2609
  tags = [y for x in tags for y in x.split(",")]
@@ -2784,6 +2875,8 @@ class AuthSrv(object):
2784
2875
  shn.dcanonical = shn._dcanonical_shr
2785
2876
  else:
2786
2877
  shn.ls = shn._ls
2878
+ shn.canonical = shn._canonical
2879
+ shn.dcanonical = shn._dcanonical
2787
2880
 
2788
2881
  shn.shr_owner = s_un
2789
2882
  shn.shr_src = (s_vfs, s_rem)
@@ -81,6 +81,7 @@ def vf_vmap() :
81
81
  }
82
82
  for k in (
83
83
  "bup_ck",
84
+ "casechk",
84
85
  "chmod_d",
85
86
  "chmod_f",
86
87
  "dbd",
@@ -244,6 +245,7 @@ flagcats = {
244
245
  "no_db_ip": "never store uploader-IP in the db; disables unpost",
245
246
  "fat32": "avoid excessive reindexing on android sdcardfs",
246
247
  "dbd=[acid|swal|wal|yolo]": "database speed-durability tradeoff",
248
+ "casechk=auto": "actively prevent case-insensitive filesystem? y/n",
247
249
  "xlink": "cross-volume dupe detection / linking (dangerous)",
248
250
  "xdev": "do not descend into other filesystems",
249
251
  "xvol": "do not follow symlinks leaving the volume root",
@@ -198,6 +198,9 @@ class FtpFs(AbstractedFS):
198
198
  if r and not cr or w and not cw or m and not cm or d and not cd:
199
199
  raise FSE(t.format(vpath), 1)
200
200
 
201
+ if "bcasechk" in vfs.flags and not vfs.casechk(rem, True):
202
+ raise FSE("No such file or directory", 1)
203
+
201
204
  return os.path.join(vfs.realpath, rem), vfs, rem
202
205
  except Pebkac as ex:
203
206
  raise FSE(str(ex))
@@ -730,6 +730,9 @@ class HttpCli(object):
730
730
  else:
731
731
  avn = vn
732
732
 
733
+ if "bcasechk" in vn.flags and not vn.casechk(rem, True):
734
+ return self.tx_404() and False
735
+
733
736
  (
734
737
  self.can_read,
735
738
  self.can_write,
@@ -1546,6 +1549,7 @@ class HttpCli(object):
1546
1549
  if xtag is not None:
1547
1550
  props = set([y.tag.split("}")[-1] for y in xtag])
1548
1551
  # assume <allprop/> otherwise; nobody ever gonna <propname/>
1552
+ self.hint = ""
1549
1553
 
1550
1554
  zi = int(time.time())
1551
1555
  vst = os.stat_result((16877, -1, -1, 1, 1000, 1000, 8, zi, zi, zi))
@@ -1555,7 +1559,9 @@ class HttpCli(object):
1555
1559
  except OSError as ex:
1556
1560
  if ex.errno not in (errno.ENOENT, errno.ENOTDIR):
1557
1561
  raise
1558
- raise Pebkac(404)
1562
+ if tap:
1563
+ raise Pebkac(404)
1564
+ st = vst
1559
1565
 
1560
1566
  topdir = {"vp": "", "st": st}
1561
1567
  fgen = []
@@ -1595,6 +1601,9 @@ class HttpCli(object):
1595
1601
  )
1596
1602
 
1597
1603
  elif depth == "0" or not stat.S_ISDIR(st.st_mode):
1604
+ if depth == "0" and not self.vpath and not vn.realpath:
1605
+ # rootless server; give dummy listing
1606
+ self.can_read = True
1598
1607
  # propfind on a file; return as topdir
1599
1608
  if not self.can_read and not self.can_get:
1600
1609
  self.log("inaccessible: %r" % ("/" + self.vpath,))
@@ -1627,7 +1636,11 @@ class HttpCli(object):
1627
1636
  self.log("inaccessible: %r" % ("/" + self.vpath,))
1628
1637
  raise Pebkac(401, "authenticate")
1629
1638
 
1630
- zi = vn.flags["du_iwho"] if "quota-available-bytes" in props else 0
1639
+ zi = (
1640
+ vn.flags["du_iwho"]
1641
+ if vn.realpath and "quota-available-bytes" in props
1642
+ else 0
1643
+ )
1631
1644
  if zi and (
1632
1645
  zi == 9
1633
1646
  or (zi == 7 and self.uname != "*")
@@ -1761,6 +1774,7 @@ class HttpCli(object):
1761
1774
  xprop = xroot.find(r"./{DAV:}propertyupdate/{DAV:}set/{DAV:}prop")
1762
1775
  for ze in xprop:
1763
1776
  ze.clear()
1777
+ self.hint = ""
1764
1778
 
1765
1779
  txt = """<multistatus xmlns="DAV:"><response><propstat><status>HTTP/1.1 403 Forbidden</status></propstat></response></multistatus>"""
1766
1780
  xroot = parse_xml(txt)
@@ -1816,6 +1830,7 @@ class HttpCli(object):
1816
1830
  ET.register_namespace("D", "DAV:")
1817
1831
  lk = parse_xml(txt)
1818
1832
  assert lk.tag == "{DAV:}lockinfo"
1833
+ self.hint = ""
1819
1834
 
1820
1835
  token = str(uuid.uuid4())
1821
1836
 
@@ -3403,8 +3418,6 @@ class HttpCli(object):
3403
3418
  sz, sha_hex, sha_b64 = copier(
3404
3419
  p_data, f, hasher, max_sz, self.args.s_wr_slp
3405
3420
  )
3406
- if sz == 0:
3407
- raise Pebkac(400, "empty files in post")
3408
3421
  finally:
3409
3422
  f.close()
3410
3423
 
@@ -4711,11 +4724,9 @@ class HttpCli(object):
4711
4724
  packer = StreamZip
4712
4725
  ext = "zip"
4713
4726
 
4714
- fn = items[0] if items and items[0] else self.vpath
4715
- if fn:
4716
- fn = fn.rstrip("/").split("/")[-1]
4717
- else:
4718
- fn = self.host.split(":")[0]
4727
+ fn = self.vpath.split("/")[-1] or self.host.split(":")[0]
4728
+ if items:
4729
+ fn = "sel-" + fn
4719
4730
 
4720
4731
  if vn.flags.get("zipmax") and not (
4721
4732
  vn.flags.get("zipmaxu") and self.uname != "*"
@@ -4891,8 +4902,8 @@ class HttpCli(object):
4891
4902
  else:
4892
4903
  fullfile = b""
4893
4904
 
4894
- if not sz_md and b"\n" in buf[:2]:
4895
- lead = buf[: buf.find(b"\n") + 1]
4905
+ if not sz_md and buf.startswith((b"\n", b"\r\n")):
4906
+ lead = b"\n" if buf.startswith(b"\n") else b"\r\n"
4896
4907
  sz_md += len(lead)
4897
4908
 
4898
4909
  sz_md += len(buf)
@@ -6207,6 +6218,7 @@ class HttpCli(object):
6207
6218
 
6208
6219
  if "v" in self.uparam:
6209
6220
  add_og = True
6221
+ og_fn = ""
6210
6222
 
6211
6223
  if "b" in self.uparam:
6212
6224
  self.out_headers["X-Robots-Tag"] = "noindex, nofollow"
@@ -12,7 +12,9 @@ from ipaddress import IPv4Network, IPv6Network
12
12
  from .__init__ import TYPE_CHECKING
13
13
  from .__init__ import unicode as U
14
14
  from .multicast import MC_Sck, MCast
15
- from .stolen.dnslib import AAAA
15
+ from .stolen.dnslib import (
16
+ AAAA,
17
+ )
16
18
  from .stolen.dnslib import CLASS as DC
17
19
  from .stolen.dnslib import (
18
20
  NSEC,
@@ -504,7 +504,6 @@ class MTag(object):
504
504
  "album-artist",
505
505
  "tpe2",
506
506
  "aart",
507
- "conductor",
508
507
  "organization",
509
508
  "band",
510
509
  ],
@@ -842,6 +842,10 @@ class SvcHub(object):
842
842
  if w8:
843
843
  time.sleep(w8)
844
844
  self.log("qr-code", qr)
845
+ if self.args.qr_stdout:
846
+ self.pr(self.tcpsrv.qr)
847
+ if self.args.qr_stderr:
848
+ self.pr(self.tcpsrv.qr, file=sys.stderr)
845
849
  w8 = self.args.qr_every
846
850
  msg = "%s\033[%dA" % (qr, len(qr.split("\n")))
847
851
  while w8:
@@ -875,8 +879,13 @@ class SvcHub(object):
875
879
  self.sticky_qr()
876
880
  if self.args.qr_wait or self.args.qr_every or self.args.qr_winch:
877
881
  Daemon(self._qr_thr, "qr")
878
- elif not self.args.qr_pin:
879
- self.log("qr-code", self.tcpsrv.qr)
882
+ else:
883
+ if not self.args.qr_pin:
884
+ self.log("qr-code", self.tcpsrv.qr)
885
+ if self.args.qr_stdout:
886
+ self.pr(self.tcpsrv.qr)
887
+ if self.args.qr_stderr:
888
+ self.pr(self.tcpsrv.qr, file=sys.stderr)
880
889
  else:
881
890
  self.log("root", "workers OK\n")
882
891
 
@@ -50,6 +50,11 @@ class U2idx(object):
50
50
  self.log("your python does not have sqlite3; searching will be disabled")
51
51
  return
52
52
 
53
+ if self.args.srch_icase:
54
+ self._open_db = self._open_db_icase
55
+ else:
56
+ self._open_db = self._open_db_std
57
+
53
58
 
54
59
  self.active_id = ""
55
60
  self.active_cur = None
@@ -65,6 +70,15 @@ class U2idx(object):
65
70
  def log(self, msg , c = 0) :
66
71
  self.log_func("u2idx", msg, c)
67
72
 
73
+ def _open_db_std(self, *args, **kwargs):
74
+ kwargs["check_same_thread"] = False
75
+ return sqlite3.connect(*args, **kwargs)
76
+
77
+ def _open_db_icase(self, *args, **kwargs):
78
+ db = self._open_db_std(*args, **kwargs)
79
+ db.create_function("casefold", 1, lambda x: x.casefold() if x else x)
80
+ return db
81
+
68
82
  def shutdown(self) :
69
83
  if not HAVE_SQLITE3:
70
84
  return
@@ -142,8 +156,7 @@ class U2idx(object):
142
156
  uri = ""
143
157
  try:
144
158
  uri = "{}?mode=ro&nolock=1".format(Path(db_path).as_uri())
145
- db = sqlite3.connect(uri, timeout=2, uri=True, check_same_thread=False)
146
- cur = db.cursor()
159
+ cur = self._open_db(uri, timeout=2, uri=True).cursor()
147
160
  cur.execute('pragma table_info("up")').fetchone()
148
161
  self.log("ro: %r" % (db_path,))
149
162
  except:
@@ -154,7 +167,7 @@ class U2idx(object):
154
167
  if not cur:
155
168
  # on windows, this steals the write-lock from up2k.deferred_init --
156
169
  # seen on win 10.0.17763.2686, py 3.10.4, sqlite 3.37.2
157
- cur = sqlite3.connect(db_path, timeout=2, check_same_thread=False).cursor()
170
+ cur = self._open_db(db_path, timeout=2).cursor()
158
171
  self.log("opened %r" % (db_path,))
159
172
 
160
173
  self.cur[ptop] = cur
@@ -167,6 +180,8 @@ class U2idx(object):
167
180
  if not HAVE_SQLITE3:
168
181
  return [], [], False
169
182
 
183
+ icase = self.args.srch_icase
184
+
170
185
  q = ""
171
186
  v = ""
172
187
  va = []
@@ -226,9 +241,13 @@ class U2idx(object):
226
241
  elif v == "path":
227
242
  v = "trim(?||up.rd,'/')"
228
243
  va.append("\nrd")
244
+ if icase:
245
+ v = "casefold(%s)" % (v,)
229
246
 
230
247
  elif v == "name":
231
248
  v = "up.fn"
249
+ if icase:
250
+ v = "casefold(%s)" % (v,)
232
251
 
233
252
  elif v == "tags" or ptn_mt.match(v):
234
253
  have_mt = True
@@ -279,6 +298,12 @@ class U2idx(object):
279
298
  tail = "||'%'"
280
299
  v = v[:-1]
281
300
 
301
+ if icase and "casefold(" in q:
302
+ try:
303
+ v = unicode(v).casefold()
304
+ except:
305
+ v = unicode(v).lower()
306
+
282
307
  q += " {}?{} ".format(head, tail)
283
308
  va.append(v)
284
309
  is_key = True
@@ -313,7 +338,7 @@ class U2idx(object):
313
338
  uname ,
314
339
  vols ,
315
340
  uq ,
316
- uv ,
341
+ uv ,
317
342
  have_mt ,
318
343
  sort ,
319
344
  lim ,