copyparty 1.19.8__tar.gz → 1.19.10__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 (125) hide show
  1. {copyparty-1.19.8 → copyparty-1.19.10}/PKG-INFO +7 -3
  2. {copyparty-1.19.8 → copyparty-1.19.10}/README.md +6 -2
  3. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/__main__.py +26 -14
  4. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/__version__.py +2 -2
  5. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/authsrv.py +102 -9
  6. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/broker_mpw.py +3 -0
  7. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/cfg.py +4 -0
  8. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/fsutil.py +66 -23
  9. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/ftpd.py +3 -0
  10. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/httpcli.py +23 -11
  11. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/mdns.py +3 -1
  12. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/mtag.py +2 -4
  13. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/svchub.py +14 -2
  14. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/u2idx.py +29 -4
  15. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/up2k.py +20 -9
  16. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/util.py +48 -22
  17. copyparty-1.19.10/copyparty/web/baguettebox.js.gz +0 -0
  18. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/web/browser.css.gz +0 -0
  19. copyparty-1.19.10/copyparty/web/browser.js.gz +0 -0
  20. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/web/dbg-audio.js.gz +0 -0
  21. copyparty-1.19.10/copyparty/web/deps/busy.mp3.gz +0 -0
  22. copyparty-1.19.10/copyparty/web/deps/easymde.css.gz +0 -0
  23. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/web/deps/easymde.js.gz +0 -0
  24. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/web/deps/marked.js.gz +0 -0
  25. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/web/deps/mini-fa.css.gz +0 -0
  26. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/web/deps/prism.css.gz +0 -0
  27. copyparty-1.19.10/copyparty/web/deps/prism.js.gz +0 -0
  28. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/web/deps/prismd.css.gz +0 -0
  29. copyparty-1.19.10/copyparty/web/deps/scp.woff2 +0 -0
  30. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/web/deps/sha512.ac.js.gz +0 -0
  31. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/web/md.css.gz +0 -0
  32. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/web/md.js.gz +0 -0
  33. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/web/md2.css.gz +0 -0
  34. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/web/md2.js.gz +0 -0
  35. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/web/mde.css.gz +0 -0
  36. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/web/mde.js.gz +0 -0
  37. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/web/msg.css.gz +0 -0
  38. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/web/rups.css.gz +0 -0
  39. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/web/rups.js.gz +0 -0
  40. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/web/shares.css.gz +0 -0
  41. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/web/shares.js.gz +0 -0
  42. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/web/splash.css.gz +0 -0
  43. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/web/splash.js.gz +0 -0
  44. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/web/svcs.js.gz +0 -0
  45. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/web/ui.css.gz +0 -0
  46. copyparty-1.19.10/copyparty/web/up2k.js.gz +0 -0
  47. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/web/util.js.gz +0 -0
  48. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/web/w.hash.js.gz +0 -0
  49. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty.egg-info/PKG-INFO +7 -3
  50. copyparty-1.19.8/copyparty/web/baguettebox.js.gz +0 -0
  51. copyparty-1.19.8/copyparty/web/browser.js.gz +0 -0
  52. copyparty-1.19.8/copyparty/web/deps/busy.mp3.gz +0 -0
  53. copyparty-1.19.8/copyparty/web/deps/easymde.css.gz +0 -0
  54. copyparty-1.19.8/copyparty/web/deps/prism.js.gz +0 -0
  55. copyparty-1.19.8/copyparty/web/deps/scp.woff2 +0 -0
  56. copyparty-1.19.8/copyparty/web/up2k.js.gz +0 -0
  57. {copyparty-1.19.8 → copyparty-1.19.10}/LICENSE +0 -0
  58. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/__init__.py +0 -0
  59. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/bos/__init__.py +0 -0
  60. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/bos/bos.py +0 -0
  61. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/bos/path.py +0 -0
  62. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/broker_mp.py +0 -0
  63. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/broker_thr.py +0 -0
  64. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/broker_util.py +0 -0
  65. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/cert.py +0 -0
  66. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/dxml.py +0 -0
  67. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/httpconn.py +0 -0
  68. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/httpsrv.py +0 -0
  69. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/ico.py +0 -0
  70. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/metrics.py +0 -0
  71. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/multicast.py +0 -0
  72. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/pwhash.py +0 -0
  73. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/res/COPYING.txt +0 -0
  74. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/res/__init__.py +0 -0
  75. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/res/insecure.pem +0 -0
  76. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/smbd.py +0 -0
  77. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/ssdp.py +0 -0
  78. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/star.py +0 -0
  79. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/stolen/__init__.py +0 -0
  80. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/stolen/dnslib/__init__.py +0 -0
  81. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/stolen/dnslib/bimap.py +0 -0
  82. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/stolen/dnslib/bit.py +0 -0
  83. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/stolen/dnslib/buffer.py +0 -0
  84. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/stolen/dnslib/dns.py +0 -0
  85. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/stolen/dnslib/label.py +0 -0
  86. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/stolen/dnslib/lex.py +0 -0
  87. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/stolen/dnslib/ranges.py +0 -0
  88. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/stolen/ifaddr/__init__.py +0 -0
  89. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/stolen/ifaddr/_posix.py +0 -0
  90. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/stolen/ifaddr/_shared.py +0 -0
  91. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/stolen/ifaddr/_win32.py +0 -0
  92. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/stolen/qrcodegen.py +0 -0
  93. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/stolen/surrogateescape.py +0 -0
  94. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/sutil.py +0 -0
  95. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/szip.py +0 -0
  96. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/tcpsrv.py +0 -0
  97. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/tftpd.py +0 -0
  98. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/th_cli.py +0 -0
  99. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/th_srv.py +0 -0
  100. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/web/a/__init__.py +0 -0
  101. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/web/a/partyfuse.py +0 -0
  102. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/web/a/u2c.py +0 -0
  103. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/web/a/webdav-cfg.bat +0 -0
  104. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/web/browser.html +0 -0
  105. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/web/browser2.html +0 -0
  106. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/web/cf.html +0 -0
  107. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/web/deps/__init__.py +0 -0
  108. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/web/deps/fuse.py +0 -0
  109. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/web/deps/mini-fa.woff +0 -0
  110. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/web/deps/sha512.hw.js.gz +0 -0
  111. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/web/idp.html +0 -0
  112. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/web/md.html +0 -0
  113. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/web/mde.html +0 -0
  114. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/web/msg.html +0 -0
  115. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/web/rups.html +0 -0
  116. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/web/shares.html +0 -0
  117. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/web/splash.html +0 -0
  118. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty/web/svcs.html +0 -0
  119. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty.egg-info/SOURCES.txt +0 -0
  120. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty.egg-info/dependency_links.txt +0 -0
  121. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty.egg-info/entry_points.txt +0 -0
  122. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty.egg-info/requires.txt +0 -0
  123. {copyparty-1.19.8 → copyparty-1.19.10}/copyparty.egg-info/top_level.txt +0 -0
  124. {copyparty-1.19.8 → copyparty-1.19.10}/pyproject.toml +0 -0
  125. {copyparty-1.19.8 → copyparty-1.19.10}/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.10
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
@@ -1418,7 +1418,7 @@ general usage:
1418
1418
  on macos, connect from finder:
1419
1419
  * [Go] -> [Connect to Server...] -> http://192.168.123.1:3923/
1420
1420
 
1421
- in order to grant full write-access to webdav clients, the volflag `daw` must be set and the account must also have delete-access (otherwise the client won't be allowed to replace the contents of existing files, which is how webdav works)
1421
+ to upload or edit files with WebDAV clients, enable the `daw` volflag (because most WebDAV clients expect this) and give your account the delete-permission. This avoids getting several copies of the same file on the server. HOWEVER: This will also make all PUT-uploads overwrite existing files if the user has delete-access, so use with caution.
1422
1422
 
1423
1423
  > note: if you have enabled [IdP authentication](#identity-providers) then that may cause issues for some/most webdav clients; see [the webdav section in the IdP docs](https://github.com/9001/copyparty/blob/hovudstraum/docs/idp.md#connecting-webdav-clients)
1424
1424
 
@@ -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=n` 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
 
@@ -3084,7 +3088,7 @@ first install one of the following:
3084
3088
  and then copypaste the following command into `a-Shell`:
3085
3089
 
3086
3090
  ```sh
3087
- curl https://github.com/9001/copyparty/raw/refs/heads/hovudstraum/contrib/setup-ashell.sh | sh
3091
+ curl -L https://github.com/9001/copyparty/raw/refs/heads/hovudstraum/contrib/setup-ashell.sh | sh
3088
3092
  ```
3089
3093
 
3090
3094
  what this does:
@@ -1353,7 +1353,7 @@ general usage:
1353
1353
  on macos, connect from finder:
1354
1354
  * [Go] -> [Connect to Server...] -> http://192.168.123.1:3923/
1355
1355
 
1356
- in order to grant full write-access to webdav clients, the volflag `daw` must be set and the account must also have delete-access (otherwise the client won't be allowed to replace the contents of existing files, which is how webdav works)
1356
+ to upload or edit files with WebDAV clients, enable the `daw` volflag (because most WebDAV clients expect this) and give your account the delete-permission. This avoids getting several copies of the same file on the server. HOWEVER: This will also make all PUT-uploads overwrite existing files if the user has delete-access, so use with caution.
1357
1357
 
1358
1358
  > note: if you have enabled [IdP authentication](#identity-providers) then that may cause issues for some/most webdav clients; see [the webdav section in the IdP docs](https://github.com/9001/copyparty/blob/hovudstraum/docs/idp.md#connecting-webdav-clients)
1359
1359
 
@@ -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=n` 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
 
@@ -3019,7 +3023,7 @@ first install one of the following:
3019
3023
  and then copypaste the following command into `a-Shell`:
3020
3024
 
3021
3025
  ```sh
3022
- curl https://github.com/9001/copyparty/raw/refs/heads/hovudstraum/contrib/setup-ashell.sh | sh
3026
+ curl -L https://github.com/9001/copyparty/raw/refs/heads/hovudstraum/contrib/setup-ashell.sh | sh
3023
3027
  ```
3024
3028
 
3025
3029
  what this does:
@@ -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)")
@@ -1209,6 +1212,7 @@ def add_upload(ap):
1209
1212
  ap2.add_argument("--chmod-d", metavar="UGO", type=u, default="755", help="unix file permissions to use when creating directories; see --help-chmod. Examples: [\033[32m755\033[0m] = owner-RW + all-R, [\033[32m777\033[0m] = full-yolo (volflag=chmod_d)")
1210
1213
  ap2.add_argument("--uid", metavar="N", type=int, default=-1, help="unix user-id to chown new files/folders to; default = -1 = do-not-change (volflag=uid)")
1211
1214
  ap2.add_argument("--gid", metavar="N", type=int, default=-1, help="unix group-id to chown new files/folders to; default = -1 = do-not-change (volflag=gid)")
1215
+ ap2.add_argument("--wram", action="store_true", help="allow uploading even if a volume is inside a ramdisk, meaning that all data will be lost on the next server reboot (volflag=wram)")
1212
1216
  ap2.add_argument("--dedup", action="store_true", help="enable symlink-based upload deduplication (volflag=dedup)")
1213
1217
  ap2.add_argument("--safe-dedup", metavar="N", type=int, default=50, help="how careful to be when deduplicating files; [\033[32m1\033[0m] = just verify the filesize, [\033[32m50\033[0m] = verify file contents have not been altered (volflag=safededup)")
1214
1218
  ap2.add_argument("--hardlink", action="store_true", help="enable hardlink-based dedup; will fallback on symlinks when that is impossible (across filesystems) (volflag=hardlink)")
@@ -1566,6 +1570,9 @@ def add_logging(ap):
1566
1570
  ap2.add_argument("--ihead", metavar="HEADER", type=u, action='append', help="print request \033[33mHEADER\033[0m; [\033[32m*\033[0m]=all")
1567
1571
  ap2.add_argument("--ohead", metavar="HEADER", type=u, action='append', help="print response \033[33mHEADER\033[0m; [\033[32m*\033[0m]=all")
1568
1572
  ap2.add_argument("--lf-url", metavar="RE", type=u, default=r"^/\.cpr/|[?&]th=[wjp]|/\.(_|ql_|DS_Store$|localized$)", help="dont log URLs matching regex \033[33mRE\033[0m")
1573
+ ap2.add_argument("--scan-st-r", metavar="SEC", type=float, default=0.1, help="fs-indexing: wait \033[33mSEC\033[0m between each status-message")
1574
+ ap2.add_argument("--scan-pr-r", metavar="SEC", type=float, default=10, help="fs-indexing: wait \033[33mSEC\033[0m between each 'progress:' message")
1575
+ ap2.add_argument("--scan-pr-s", metavar="MiB", type=float, default=1, help="fs-indexing: say 'file: <name>' when a file larger than \033[33mMiB\033[0m is about to be hashed")
1569
1576
 
1570
1577
 
1571
1578
  def add_admin(ap):
@@ -1673,6 +1680,7 @@ def add_db_general(ap, hcores):
1673
1680
  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
1681
  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
1682
  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, ...)")
1683
+ 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
1684
  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
1685
  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
1686
  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 +1735,7 @@ def add_og(ap):
1727
1735
  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
1736
 
1729
1737
 
1730
- def add_ui(ap, retry):
1738
+ def add_ui(ap, retry ):
1731
1739
  THEMES = 10
1732
1740
  ap2 = ap.add_argument_group("ui options")
1733
1741
  ap2.add_argument("--grid", action="store_true", help="show grid/thumbnails by default (volflag=grid)")
@@ -1869,18 +1877,21 @@ def run_argparse(
1869
1877
  for k, h, _ in sects:
1870
1878
  ap2.add_argument("--help-" + k, action="store_true", help=h)
1871
1879
 
1872
- try:
1873
- if not retry:
1874
- raise Exception()
1875
-
1880
+ if retry:
1881
+ a = ["ascii", "replace"]
1876
1882
  for x in ap._actions:
1877
- if not x.help:
1878
- continue
1883
+ try:
1884
+ x.default = x.default.encode(*a).decode(*a)
1885
+ except:
1886
+ pass
1879
1887
 
1880
- a = ["ascii", "replace"]
1881
- x.help = x.help.encode(*a).decode(*a) + "\033[0m"
1882
- except:
1883
- pass
1888
+ try:
1889
+ if x.help and x.help is not argparse.SUPPRESS:
1890
+ x.help = x.help.replace("└─", "`-").encode(*a).decode(*a)
1891
+ if retry > 2:
1892
+ x.help = RE_ANSI.sub("", x.help)
1893
+ except:
1894
+ pass
1884
1895
 
1885
1896
  ret = ap.parse_args(args=argv[1:])
1886
1897
  for k, h, t in sects:
@@ -1990,7 +2001,7 @@ def main(argv = None) :
1990
2001
  except:
1991
2002
  nc = 486 # mdns/ssdp restart headroom; select() maxfd is 512 on windows
1992
2003
 
1993
- retry = False
2004
+ retry = 0
1994
2005
  for fmtr in [RiceFormatter, RiceFormatter, Dodge11874, BasicDodge11874]:
1995
2006
  try:
1996
2007
  al = run_argparse(argv, fmtr, retry, nc)
@@ -1999,8 +2010,9 @@ def main(argv = None) :
1999
2010
  except SystemExit:
2000
2011
  raise
2001
2012
  except:
2002
- retry = True
2003
- lprint("\n[ {} ]:\n{}\n".format(fmtr, min_ex()))
2013
+ retry += 1
2014
+ 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"
2015
+ lprint(t % (fmtr, min_ex()))
2004
2016
 
2005
2017
  try:
2006
2018
  assert al # type: ignore
@@ -1,8 +1,8 @@
1
1
  # coding: utf-8
2
2
 
3
- VERSION = (1, 19, 8)
3
+ VERSION = (1, 19, 10)
4
4
  CODENAME = "usernames"
5
- BUILD_DT = (2025, 9, 7)
5
+ BUILD_DT = (2025, 9, 19)
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)
@@ -12,6 +12,7 @@ import queue
12
12
  from .__init__ import ANYWIN
13
13
  from .authsrv import AuthSrv
14
14
  from .broker_util import BrokerCli, ExceptionalQueue, NotExQueue
15
+ from .fsutil import ramdisk_chk
15
16
  from .httpsrv import HttpSrv
16
17
  from .util import FAKE_MP, Daemon, HMaccas
17
18
 
@@ -50,6 +51,7 @@ class MpWorker(BrokerCli):
50
51
 
51
52
  # starting to look like a good idea
52
53
  self.asrv = AuthSrv(args, None, False)
54
+ ramdisk_chk(self.asrv)
53
55
 
54
56
  # instantiate all services here (TODO: inheritance?)
55
57
  self.iphash = HMaccas(os.path.join(self.args.E.cfg, "iphash"), 8)
@@ -93,6 +95,7 @@ class MpWorker(BrokerCli):
93
95
  if dest == "reload":
94
96
  self.logw("mpw.asrv reloading")
95
97
  self.asrv.reload()
98
+ ramdisk_chk(self.asrv)
96
99
  self.logw("mpw.asrv reloaded")
97
100
  continue
98
101
 
@@ -57,6 +57,7 @@ def vf_bmap() :
57
57
  "rmagic",
58
58
  "rss",
59
59
  "wo_up_readme",
60
+ "wram",
60
61
  "xdev",
61
62
  "xlink",
62
63
  "xvol",
@@ -81,6 +82,7 @@ def vf_vmap() :
81
82
  }
82
83
  for k in (
83
84
  "bup_ck",
85
+ "casechk",
84
86
  "chmod_d",
85
87
  "chmod_f",
86
88
  "dbd",
@@ -186,6 +188,7 @@ flagcats = {
186
188
  "chmod_f=644": "unix-permission for new files",
187
189
  "uid=573": "change owner of new files/folders to unix-user 573",
188
190
  "gid=999": "change owner of new files/folders to unix-group 999",
191
+ "wram": "allow uploading into ramdisks",
189
192
  "sparse": "force use of sparse files, mainly for s3-backed storage",
190
193
  "nosparse": "deny use of sparse files, mainly for slow storage",
191
194
  "daw": "enable full WebDAV write support (dangerous);\nPUT-operations will now \033[1;31mOVERWRITE\033[0;35m existing files",
@@ -244,6 +247,7 @@ flagcats = {
244
247
  "no_db_ip": "never store uploader-IP in the db; disables unpost",
245
248
  "fat32": "avoid excessive reindexing on android sdcardfs",
246
249
  "dbd=[acid|swal|wal|yolo]": "database speed-durability tradeoff",
250
+ "casechk=auto": "actively prevent case-insensitive filesystem? y/n",
247
251
  "xlink": "cross-volume dupe detection / linking (dangerous)",
248
252
  "xdev": "do not descend into other filesystems",
249
253
  "xvol": "do not follow symlinks leaving the volume root",
@@ -7,27 +7,30 @@ import re
7
7
  import time
8
8
 
9
9
  from .__init__ import ANYWIN, MACOS
10
- from .authsrv import AXS, VFS
10
+ from .authsrv import AXS, VFS, AuthSrv
11
11
  from .bos import bos
12
12
  from .util import chkcmd, min_ex, undot
13
13
 
14
14
  class Fstab(object):
15
- def __init__(self, log , args ):
15
+ def __init__(self, log , args , verbose ):
16
16
  self.log_func = log
17
+ self.verbose = verbose
17
18
 
18
19
  self.warned = False
19
20
  self.trusted = False
20
21
  self.tab = None
21
22
  self.oldtab = None
22
23
  self.srctab = "a"
23
- self.cache = {}
24
+ self.cache = {}
24
25
  self.age = 0.0
25
26
  self.maxage = args.mtab_age
26
27
 
27
28
  def log(self, msg , c = 0) :
29
+ if not c or self.verbose:
30
+ return
28
31
  self.log_func("fstab", msg, c)
29
32
 
30
- def get(self, path ) :
33
+ def get(self, path ) :
31
34
  now = time.time()
32
35
  if now - self.age > self.maxage or len(self.cache) > 9000:
33
36
  self.age = now
@@ -35,6 +38,7 @@ class Fstab(object):
35
38
  self.tab = None
36
39
  self.cache = {}
37
40
 
41
+ mp = ""
38
42
  fs = "ext4"
39
43
  msg = "failed to determine filesystem at %r; assuming %s\n%s"
40
44
 
@@ -44,7 +48,7 @@ class Fstab(object):
44
48
  path = self._winpath(path)
45
49
  except:
46
50
  self.log(msg % (path, fs, min_ex()), 3)
47
- return fs
51
+ return fs, ""
48
52
 
49
53
  path = undot(path)
50
54
  try:
@@ -53,14 +57,14 @@ class Fstab(object):
53
57
  pass
54
58
 
55
59
  try:
56
- fs = self.get_w32(path) if ANYWIN else self.get_unix(path)
60
+ fs, mp = self.get_w32(path) if ANYWIN else self.get_unix(path)
57
61
  except:
58
62
  self.log(msg % (path, fs, min_ex()), 3)
59
63
 
60
64
  fs = fs.lower()
61
- self.cache[path] = fs
62
- self.log("found %s at %r" % (fs, path))
63
- return fs
65
+ self.cache[path] = (fs, mp)
66
+ self.log("found %s at %r, %r" % (fs, mp, path))
67
+ return fs, mp
64
68
 
65
69
  def _winpath(self, path ) :
66
70
  # try to combine volume-label + st_dev (vsn)
@@ -75,34 +79,49 @@ class Fstab(object):
75
79
  self.tab = VFS(self.log_func, "idk", "/", "/", AXS(), {})
76
80
  self.trusted = False
77
81
 
78
- def build_tab(self) :
79
- self.log("inspecting mtab for changes")
80
-
82
+ def _from_sp_mount(self) :
81
83
  sptn = r"^.*? on (.*) type ([^ ]+) \(.*"
82
84
  if MACOS:
83
85
  sptn = r"^.*? on (.*) \(([^ ]+), .*"
84
86
 
85
87
  ptn = re.compile(sptn)
86
88
  so, _ = chkcmd(["mount"])
87
- tab1 = []
88
- atab = []
89
+ dtab = {}
89
90
  for ln in so.split("\n"):
90
91
  m = ptn.match(ln)
91
92
  if not m:
92
93
  continue
93
94
 
94
95
  zs1, zs2 = m.groups()
95
- tab1.append((str(zs1), str(zs2)))
96
- atab.append(ln)
96
+ dtab[str(zs1)] = str(zs2)
97
+
98
+ return dtab
99
+
100
+ def _from_proc(self) :
101
+ ret = {}
102
+ with open("/proc/self/mounts", "rb", 262144) as f:
103
+ src = f.read(262144).decode("utf-8", "replace").split("\n")
104
+ for zsl in [x.split(" ") for x in src]:
105
+ if len(zsl) < 3:
106
+ continue
107
+ zs = zsl[1]
108
+ zs = zs.replace("\\011", "\t").replace("\\040", " ").replace("\\134", "\\")
109
+ ret[zs] = zsl[2]
110
+ return ret
111
+
112
+ def build_tab(self) :
113
+ self.log("inspecting mtab for changes")
114
+ dtab = self._from_sp_mount() if MACOS else self._from_proc()
97
115
 
98
116
  # keep empirically-correct values if mounttab unchanged
99
- srctab = "\n".join(sorted(atab))
117
+ srctab = str(sorted(dtab.items()))
100
118
  if srctab == self.srctab:
101
119
  self.tab = self.oldtab
102
120
  return
103
121
 
104
122
  self.log("mtab has changed; reevaluating support for sparse files")
105
123
 
124
+ tab1 = list(dtab.items())
106
125
  tab1.sort(key=lambda x: (len(x[0]), x[0]))
107
126
  path1, fs1 = tab1[0]
108
127
  tab = VFS(self.log_func, fs1, path1, path1, AXS(), {})
@@ -139,7 +158,7 @@ class Fstab(object):
139
158
  vn.realpath = ptn.sub(nval, vn.realpath)
140
159
  visit.extend(list(vn.nodes.values()))
141
160
 
142
- def get_unix(self, path ) :
161
+ def get_unix(self, path ) :
143
162
  if not self.tab:
144
163
  try:
145
164
  self.build_tab()
@@ -148,18 +167,42 @@ class Fstab(object):
148
167
  # prisonparty or other restrictive environment
149
168
  if not self.warned:
150
169
  self.warned = True
151
- self.log("failed to build tab:\n{}".format(min_ex()), 3)
170
+ t = "failed to associate fs-mounts with the VFS (this is fine):\n%s"
171
+ self.log(t % (min_ex(),), 6)
152
172
  self.build_fallback()
153
173
 
154
174
  ret = self.tab._find(path)[0]
155
175
  if self.trusted or path == ret.vpath:
156
- return ret.realpath.split("/")[0]
176
+ return ret.realpath.split("/")[0], ret.vpath
157
177
  else:
158
- return "idk"
178
+ return "idk", ""
159
179
 
160
- def get_w32(self, path ) :
180
+ def get_w32(self, path ) :
161
181
  if not self.tab:
162
182
  self.build_fallback()
163
183
 
164
184
  ret = self.tab._find(path)[0]
165
- return ret.realpath
185
+ return ret.realpath, ""
186
+
187
+
188
+ def ramdisk_chk(asrv ) :
189
+ # should have been in authsrv but that's a circular import
190
+ mods = []
191
+ ramfs = ("tmpfs", "overlay")
192
+ log = asrv.log_func or print
193
+ fstab = Fstab(log, asrv.args, False)
194
+ for vn in asrv.vfs.all_nodes.values():
195
+ if not vn.axs.uwrite or "wram" in vn.flags:
196
+ continue
197
+ ap = vn.realpath
198
+ if not ap or os.path.isfile(ap):
199
+ continue
200
+ fs, mp = fstab.get(ap)
201
+ mp = "/" + mp.strip("/")
202
+ if fs == "tmpfs" or (mp == "/" and fs in ramfs):
203
+ mods.append((vn.vpath, ap, fs, mp))
204
+ vn.axs.uwrite.clear()
205
+ if mods:
206
+ t = "WARNING: write-access was removed from the following volumes because they are not mapped to an actual HDD for storage! All uploaded data would live in RAM only, and all uploaded files would be LOST on next reboot. To allow uploading and ignore this hazard, enable the 'wram' option (global/volflag). List of affected volumes:"
207
+ t2 = ["\n volume=[/%s], abspath=%r, type=%s, root=%r" % x for x in mods]
208
+ log("vfs", t + "".join(t2) + "\n", 1)
@@ -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))