copyparty 1.19.7__tar.gz → 1.19.8__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 (127) hide show
  1. {copyparty-1.19.7 → copyparty-1.19.8}/PKG-INFO +8 -1
  2. {copyparty-1.19.7 → copyparty-1.19.8}/README.md +7 -0
  3. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/__init__.py +2 -0
  4. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/__main__.py +38 -13
  5. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/__version__.py +2 -2
  6. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/authsrv.py +96 -8
  7. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/bos/bos.py +38 -2
  8. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/cfg.py +4 -0
  9. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/ftpd.py +4 -25
  10. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/httpcli.py +62 -24
  11. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/httpsrv.py +1 -1
  12. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/smbd.py +1 -1
  13. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/svchub.py +27 -1
  14. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/tcpsrv.py +4 -0
  15. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/up2k.py +49 -25
  16. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/util.py +8 -5
  17. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/web/a/partyfuse.py +19 -11
  18. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/web/a/u2c.py +6 -4
  19. copyparty-1.19.8/copyparty/web/baguettebox.js.gz +0 -0
  20. copyparty-1.19.8/copyparty/web/browser.css.gz +0 -0
  21. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/web/browser.html +1 -1
  22. copyparty-1.19.8/copyparty/web/browser.js.gz +0 -0
  23. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/web/idp.html +5 -5
  24. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/web/md.html +1 -1
  25. copyparty-1.19.8/copyparty/web/md.js.gz +0 -0
  26. copyparty-1.19.8/copyparty/web/md2.js.gz +0 -0
  27. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/web/mde.html +1 -1
  28. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/web/msg.html +1 -1
  29. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/web/rups.html +1 -1
  30. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/web/shares.html +5 -5
  31. copyparty-1.19.8/copyparty/web/splash.css.gz +0 -0
  32. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/web/splash.html +54 -38
  33. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/web/splash.js.gz +0 -0
  34. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/web/svcs.html +80 -39
  35. copyparty-1.19.8/copyparty/web/svcs.js.gz +0 -0
  36. copyparty-1.19.8/copyparty/web/up2k.js.gz +0 -0
  37. copyparty-1.19.8/copyparty/web/util.js.gz +0 -0
  38. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty.egg-info/PKG-INFO +8 -1
  39. copyparty-1.19.7/copyparty/web/baguettebox.js.gz +0 -0
  40. copyparty-1.19.7/copyparty/web/browser.css.gz +0 -0
  41. copyparty-1.19.7/copyparty/web/browser.js.gz +0 -0
  42. copyparty-1.19.7/copyparty/web/md.js.gz +0 -0
  43. copyparty-1.19.7/copyparty/web/md2.js.gz +0 -0
  44. copyparty-1.19.7/copyparty/web/splash.css.gz +0 -0
  45. copyparty-1.19.7/copyparty/web/svcs.js.gz +0 -0
  46. copyparty-1.19.7/copyparty/web/up2k.js.gz +0 -0
  47. copyparty-1.19.7/copyparty/web/util.js.gz +0 -0
  48. {copyparty-1.19.7 → copyparty-1.19.8}/LICENSE +0 -0
  49. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/bos/__init__.py +0 -0
  50. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/bos/path.py +0 -0
  51. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/broker_mp.py +0 -0
  52. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/broker_mpw.py +0 -0
  53. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/broker_thr.py +0 -0
  54. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/broker_util.py +0 -0
  55. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/cert.py +0 -0
  56. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/dxml.py +0 -0
  57. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/fsutil.py +0 -0
  58. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/httpconn.py +0 -0
  59. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/ico.py +0 -0
  60. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/mdns.py +0 -0
  61. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/metrics.py +0 -0
  62. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/mtag.py +0 -0
  63. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/multicast.py +0 -0
  64. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/pwhash.py +0 -0
  65. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/res/COPYING.txt +0 -0
  66. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/res/__init__.py +0 -0
  67. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/res/insecure.pem +0 -0
  68. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/ssdp.py +0 -0
  69. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/star.py +0 -0
  70. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/stolen/__init__.py +0 -0
  71. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/stolen/dnslib/__init__.py +0 -0
  72. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/stolen/dnslib/bimap.py +0 -0
  73. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/stolen/dnslib/bit.py +0 -0
  74. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/stolen/dnslib/buffer.py +0 -0
  75. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/stolen/dnslib/dns.py +0 -0
  76. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/stolen/dnslib/label.py +0 -0
  77. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/stolen/dnslib/lex.py +0 -0
  78. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/stolen/dnslib/ranges.py +0 -0
  79. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/stolen/ifaddr/__init__.py +0 -0
  80. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/stolen/ifaddr/_posix.py +0 -0
  81. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/stolen/ifaddr/_shared.py +0 -0
  82. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/stolen/ifaddr/_win32.py +0 -0
  83. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/stolen/qrcodegen.py +0 -0
  84. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/stolen/surrogateescape.py +0 -0
  85. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/sutil.py +0 -0
  86. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/szip.py +0 -0
  87. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/tftpd.py +0 -0
  88. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/th_cli.py +0 -0
  89. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/th_srv.py +0 -0
  90. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/u2idx.py +0 -0
  91. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/web/a/__init__.py +0 -0
  92. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/web/a/webdav-cfg.bat +0 -0
  93. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/web/browser2.html +0 -0
  94. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/web/cf.html +0 -0
  95. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/web/dbg-audio.js.gz +0 -0
  96. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/web/deps/__init__.py +0 -0
  97. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/web/deps/busy.mp3.gz +0 -0
  98. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/web/deps/easymde.css.gz +0 -0
  99. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/web/deps/easymde.js.gz +0 -0
  100. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/web/deps/fuse.py +0 -0
  101. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/web/deps/marked.js.gz +0 -0
  102. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/web/deps/mini-fa.css.gz +0 -0
  103. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/web/deps/mini-fa.woff +0 -0
  104. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/web/deps/prism.css.gz +0 -0
  105. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/web/deps/prism.js.gz +0 -0
  106. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/web/deps/prismd.css.gz +0 -0
  107. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/web/deps/scp.woff2 +0 -0
  108. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/web/deps/sha512.ac.js.gz +0 -0
  109. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/web/deps/sha512.hw.js.gz +0 -0
  110. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/web/md.css.gz +0 -0
  111. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/web/md2.css.gz +0 -0
  112. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/web/mde.css.gz +0 -0
  113. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/web/mde.js.gz +0 -0
  114. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/web/msg.css.gz +0 -0
  115. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/web/rups.css.gz +0 -0
  116. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/web/rups.js.gz +0 -0
  117. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/web/shares.css.gz +0 -0
  118. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/web/shares.js.gz +0 -0
  119. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/web/ui.css.gz +0 -0
  120. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty/web/w.hash.js.gz +0 -0
  121. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty.egg-info/SOURCES.txt +0 -0
  122. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty.egg-info/dependency_links.txt +0 -0
  123. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty.egg-info/entry_points.txt +0 -0
  124. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty.egg-info/requires.txt +0 -0
  125. {copyparty-1.19.7 → copyparty-1.19.8}/copyparty.egg-info/top_level.txt +0 -0
  126. {copyparty-1.19.7 → copyparty-1.19.8}/pyproject.toml +0 -0
  127. {copyparty-1.19.7 → copyparty-1.19.8}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: copyparty
3
- Version: 1.19.7
3
+ Version: 1.19.8
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
@@ -1393,6 +1393,8 @@ some recommended FTP / FTPS clients; `wark` = example password:
1393
1393
  * https://rclone.org/ does FTPS with `tls=false explicit_tls=true`
1394
1394
  * `lftp -u k,wark -p 3921 127.0.0.1 -e ls`
1395
1395
  * `lftp -u k,wark -p 3990 127.0.0.1 -e 'set ssl:verify-certificate no; ls'`
1396
+ * `curl ftp://127.0.0.1:3921/` (plaintext ftp)
1397
+ * `curl --ssl-reqd ftp://127.0.0.1:3990/` (encrypted ftps)
1396
1398
 
1397
1399
  config file example, which restricts FTP to only use ports 3921 and 12000-12099 so all of those ports must be opened in your firewall:
1398
1400
 
@@ -2005,6 +2007,10 @@ you can disable the built-in password-based login system, and instead replace it
2005
2007
 
2006
2008
  * the regular config-defined users will be used as a fallback for requests which don't include a valid (trusted) IdP username header
2007
2009
 
2010
+ * `--auth-ord` configured auth precedence, for example to allow overriding the IdP with a copyparty password
2011
+
2012
+ * the login/logout links/buttons can be replaced with links to your IdP with `--idp-login` and `--idp-logout` , for example `--idp-login /idp/login/?redir={dst}` will expand `{dst}` to the page the user was on when clicking Login
2013
+
2008
2014
  * if your IdP-server is slow, consider `--idp-cookie` and let requests with the cookie `cppws` bypass the IdP; experimental sessions-based feature added for a party
2009
2015
 
2010
2016
  some popular identity providers are [Authelia](https://www.authelia.com/) (config-file based) and [authentik](https://goauthentik.io/) (GUI-based, more complex)
@@ -2381,6 +2387,7 @@ buggy feature? rip it out by setting any of the following environment variables
2381
2387
  | `PRTY_NO_SQLITE` | disable all database-related functionality (file indexing, metadata indexing, most file deduplication logic) |
2382
2388
  | `PRTY_NO_TLS` | disable native HTTPS support; if you still want to accept HTTPS connections then TLS must now be terminated by a reverse-proxy |
2383
2389
  | `PRTY_NO_TPOKE` | disable systemd-tmpfilesd avoider |
2390
+ | `PRTY_UNSAFE_STATE` | allow storing secrets into emergency-fallback locations |
2384
2391
 
2385
2392
  example: `PRTY_NO_IFADDR=1 python3 copyparty-sfx.py`
2386
2393
 
@@ -1328,6 +1328,8 @@ some recommended FTP / FTPS clients; `wark` = example password:
1328
1328
  * https://rclone.org/ does FTPS with `tls=false explicit_tls=true`
1329
1329
  * `lftp -u k,wark -p 3921 127.0.0.1 -e ls`
1330
1330
  * `lftp -u k,wark -p 3990 127.0.0.1 -e 'set ssl:verify-certificate no; ls'`
1331
+ * `curl ftp://127.0.0.1:3921/` (plaintext ftp)
1332
+ * `curl --ssl-reqd ftp://127.0.0.1:3990/` (encrypted ftps)
1331
1333
 
1332
1334
  config file example, which restricts FTP to only use ports 3921 and 12000-12099 so all of those ports must be opened in your firewall:
1333
1335
 
@@ -1940,6 +1942,10 @@ you can disable the built-in password-based login system, and instead replace it
1940
1942
 
1941
1943
  * the regular config-defined users will be used as a fallback for requests which don't include a valid (trusted) IdP username header
1942
1944
 
1945
+ * `--auth-ord` configured auth precedence, for example to allow overriding the IdP with a copyparty password
1946
+
1947
+ * the login/logout links/buttons can be replaced with links to your IdP with `--idp-login` and `--idp-logout` , for example `--idp-login /idp/login/?redir={dst}` will expand `{dst}` to the page the user was on when clicking Login
1948
+
1943
1949
  * if your IdP-server is slow, consider `--idp-cookie` and let requests with the cookie `cppws` bypass the IdP; experimental sessions-based feature added for a party
1944
1950
 
1945
1951
  some popular identity providers are [Authelia](https://www.authelia.com/) (config-file based) and [authentik](https://goauthentik.io/) (GUI-based, more complex)
@@ -2316,6 +2322,7 @@ buggy feature? rip it out by setting any of the following environment variables
2316
2322
  | `PRTY_NO_SQLITE` | disable all database-related functionality (file indexing, metadata indexing, most file deduplication logic) |
2317
2323
  | `PRTY_NO_TLS` | disable native HTTPS support; if you still want to accept HTTPS connections then TLS must now be terminated by a reverse-proxy |
2318
2324
  | `PRTY_NO_TPOKE` | disable systemd-tmpfilesd avoider |
2325
+ | `PRTY_UNSAFE_STATE` | allow storing secrets into emergency-fallback locations |
2319
2326
 
2320
2327
  example: `PRTY_NO_IFADDR=1 python3 copyparty-sfx.py`
2321
2328
 
@@ -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()
@@ -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
  ],
@@ -1169,6 +1185,7 @@ def add_share(ap):
1169
1185
  ap2 = ap.add_argument_group("share-url options")
1170
1186
  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
1187
  ap2.add_argument("--shr-db", metavar="FILE", type=u, default=db_path, help="database to store shares in")
1188
+ 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
1189
  ap2.add_argument("--shr-adm", metavar="U,U", type=u, default="", help="comma-separated list of users allowed to view/delete any share")
1173
1190
  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
1191
  ap2.add_argument("--shr-v", action="store_true", help="debug")
@@ -1285,6 +1302,9 @@ def add_auth(ap):
1285
1302
  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
1303
  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
1304
  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)")
1305
+ 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")
1306
+ ap2.add_argument("--idp-login-t", metavar="T", type=u, default="Login with SSO", help="the label/text for the idp-login button")
1307
+ 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
1308
  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
1309
  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
1310
  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 +1319,7 @@ def add_auth(ap):
1299
1319
  ap2.add_argument("--ao-idp-before-pw", type=u, default="", help=argparse.SUPPRESS)
1300
1320
  ap2.add_argument("--ao-h-before-hm", type=u, default="", help=argparse.SUPPRESS)
1301
1321
  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)
1322
+ ap2.add_argument("--ao-have-pw", type=u, default="", help=argparse.SUPPRESS)
1303
1323
 
1304
1324
 
1305
1325
  def add_chpw(ap):
@@ -1445,6 +1465,7 @@ def add_yolo(ap):
1445
1465
  ap2.add_argument("--no-fnugg", action="store_true", help="disable the smoketest for caching-related issues in the web-UI")
1446
1466
  ap2.add_argument("--getmod", action="store_true", help="permit ?move=[...] and ?delete as GET")
1447
1467
  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)")
1468
+ 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
1469
 
1449
1470
 
1450
1471
  def add_optouts(ap):
@@ -1458,7 +1479,7 @@ def add_optouts(ap):
1458
1479
  ap2.add_argument("--no-fs-abrt", action="store_true", help="disable ability to abort ongoing copy/move")
1459
1480
  ap2.add_argument("-nth", action="store_true", help="no title hostname; don't show \033[33m--name\033[0m in <title>")
1460
1481
  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")
1482
+ 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
1483
  ap2.add_argument("-nb", action="store_true", help="no powered-by-copyparty branding in UI")
1463
1484
  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
1485
  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)")
@@ -1736,7 +1757,11 @@ def add_ui(ap, retry):
1736
1757
  ap2.add_argument("--doctitle", metavar="TXT", type=u, default="copyparty @ --name", help="title / service-name to show in html documents")
1737
1758
  ap2.add_argument("--bname", metavar="TXT", type=u, default="--name", help="server name (displayed in filebrowser document title)")
1738
1759
  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)")
1760
+ 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")
1761
+ 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")
1762
+ 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)")
1763
+ ap2.add_argument("--ver-iwho", type=int, default=0, help=argparse.SUPPRESS)
1764
+ ap2.add_argument("--du-iwho", type=int, default=0, help=argparse.SUPPRESS)
1740
1765
  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
1766
  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
1767
  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")
@@ -1,8 +1,8 @@
1
1
  # coding: utf-8
2
2
 
3
- VERSION = (1, 19, 7)
3
+ VERSION = (1, 19, 8)
4
4
  CODENAME = "usernames"
5
- BUILD_DT = (2025, 8, 28)
5
+ BUILD_DT = (2025, 9, 7)
6
6
 
7
7
  S_VERSION = ".".join(map(str, VERSION))
8
8
  S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)
@@ -424,6 +424,8 @@ class VFS(object):
424
424
 
425
425
  self.get_dbv = self._get_dbv
426
426
  self.ls = self._ls
427
+ self.canonical = self._canonical
428
+ self.dcanonical = self._dcanonical
427
429
 
428
430
  def __repr__(self) :
429
431
  return "VFS(%s)" % (
@@ -617,7 +619,7 @@ class VFS(object):
617
619
  vrem = vjoin(self.vpath[len(dbv.vpath) :].lstrip("/"), vrem)
618
620
  return dbv, vrem
619
621
 
620
- def canonical(self, rem , resolve = True) :
622
+ def _canonical(self, rem , resolve = True) :
621
623
  """returns the canonical path (fully-resolved absolute fs path)"""
622
624
  ap = self.realpath
623
625
  if rem:
@@ -625,7 +627,7 @@ class VFS(object):
625
627
 
626
628
  return absreal(ap) if resolve else ap
627
629
 
628
- def dcanonical(self, rem ) :
630
+ def _dcanonical(self, rem ) :
629
631
  """resolves until the final component (filename)"""
630
632
  ap = self.realpath
631
633
  if rem:
@@ -634,6 +636,42 @@ class VFS(object):
634
636
  ad, fn = os.path.split(ap)
635
637
  return os.path.join(absreal(ad), fn)
636
638
 
639
+ def _canonical_shr(self, rem , resolve = True) :
640
+ """returns the canonical path (fully-resolved absolute fs path)"""
641
+ ap = self.realpath
642
+ if rem:
643
+ ap += "/" + rem
644
+
645
+ rap = absreal(ap)
646
+ if self.shr_files:
647
+ vn, rem = self.shr_src
648
+ chk = absreal(os.path.join(vn.realpath, rem))
649
+ if chk != rap:
650
+ # not the dir itself; assert file allowed
651
+ ad, fn = os.path.split(rap)
652
+ if chk != ad or fn not in self.shr_files:
653
+ return "\n\n"
654
+
655
+ return rap if resolve else ap
656
+
657
+ def _dcanonical_shr(self, rem ) :
658
+ """resolves until the final component (filename)"""
659
+ ap = self.realpath
660
+ if rem:
661
+ ap += "/" + rem
662
+
663
+ ad, fn = os.path.split(ap)
664
+ ad = absreal(ad)
665
+ if self.shr_files:
666
+ vn, rem = self.shr_src
667
+ chk = absreal(os.path.join(vn.realpath, rem))
668
+ if chk != absreal(ap):
669
+ # not the dir itself; assert file allowed
670
+ if ad != chk or fn not in self.shr_files:
671
+ return "\n\n"
672
+
673
+ return os.path.join(ad, fn)
674
+
637
675
  def _ls_nope(
638
676
  self, *a, **ka
639
677
  ) :
@@ -969,6 +1007,14 @@ class AuthSrv(object):
969
1007
  self.indent = ""
970
1008
  self.is_lxc = args.c == ["/z/initcfg"]
971
1009
 
1010
+ self._vf0b = {
1011
+ "tcolor": self.args.tcolor,
1012
+ "du_iwho": self.args.du_iwho,
1013
+ "shr_who": self.args.shr_who if self.args.shr else "no",
1014
+ }
1015
+ self._vf0 = self._vf0b.copy()
1016
+ self._vf0["d2d"] = True
1017
+
972
1018
  # fwd-decl
973
1019
  self.vfs = VFS(log_func, "", "", "", AXS(), {})
974
1020
  self.acct = {} # uname->pw
@@ -1007,7 +1053,10 @@ class AuthSrv(object):
1007
1053
  yield prev, True
1008
1054
 
1009
1055
  def vf0(self):
1010
- return {"d2d": True, "tcolor": self.args.tcolor}
1056
+ return self._vf0.copy()
1057
+
1058
+ def vf0b(self):
1059
+ return self._vf0b.copy()
1011
1060
 
1012
1061
  def idp_checkin(
1013
1062
  self, broker , uname , gname
@@ -1743,12 +1792,15 @@ class AuthSrv(object):
1743
1792
  files = os.listdir(E.cfg)
1744
1793
  except:
1745
1794
  files = []
1746
- hits = [x for x in files if x.lower().endswith(".conf")]
1795
+ hits = [
1796
+ x
1797
+ for x in files
1798
+ if x.lower().endswith(".conf") and not x.startswith(".")
1799
+ ]
1747
1800
  if hits:
1748
1801
  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
1802
  self.log(t % (E.cfg, ehint, ", ".join(hits)), 3)
1750
- zvf = {"tcolor": self.args.tcolor}
1751
- vfs = VFS(self.log_func, absreal("."), "", "", axs, zvf)
1803
+ vfs = VFS(self.log_func, absreal("."), "", "", axs, self.vf0b())
1752
1804
  if not axs.uread:
1753
1805
  self.badcfg1 = True
1754
1806
  elif "" not in mount:
@@ -2281,6 +2333,11 @@ class AuthSrv(object):
2281
2333
  vol.lim.uid = vol.flags["uid"]
2282
2334
  vol.lim.gid = vol.flags["gid"]
2283
2335
 
2336
+ vol.flags["du_iwho"] = n_du_who(vol.flags["du_who"])
2337
+
2338
+ if not enshare:
2339
+ vol.flags["shr_who"] = "no"
2340
+
2284
2341
  if vol.flags.get("og"):
2285
2342
  self.args.uqe = True
2286
2343
 
@@ -2723,6 +2780,8 @@ class AuthSrv(object):
2723
2780
 
2724
2781
  shn.shr_files = set(fns)
2725
2782
  shn.ls = shn._ls_shr
2783
+ shn.canonical = shn._canonical_shr
2784
+ shn.dcanonical = shn._dcanonical_shr
2726
2785
  else:
2727
2786
  shn.ls = shn._ls
2728
2787
 
@@ -2787,6 +2846,7 @@ class AuthSrv(object):
2787
2846
  "dcrop": vf["crop"],
2788
2847
  "dth3x": vf["th3x"],
2789
2848
  "u2ts": vf["u2ts"],
2849
+ "shr_who": vf["shr_who"],
2790
2850
  "frand": bool(vf.get("rand")),
2791
2851
  "lifetime": vf.get("lifetime") or 0,
2792
2852
  "unlist": vf.get("unlist") or "",
@@ -2795,11 +2855,13 @@ class AuthSrv(object):
2795
2855
  js_htm = {
2796
2856
  "SPINNER": self.args.spinner,
2797
2857
  "s_name": self.args.bname,
2858
+ "idp_login": self.args.idp_login,
2798
2859
  "have_up2k_idx": "e2d" in vf,
2799
2860
  "have_acode": not self.args.no_acode,
2800
2861
  "have_c2flac": self.args.allow_flac,
2801
2862
  "have_c2wav": self.args.allow_wav,
2802
2863
  "have_shr": self.args.shr,
2864
+ "shr_who": vf["shr_who"],
2803
2865
  "have_zip": not self.args.no_zip,
2804
2866
  "have_mv": not self.args.no_mv,
2805
2867
  "have_del": not self.args.no_del,
@@ -2865,7 +2927,7 @@ class AuthSrv(object):
2865
2927
  self.args.ao_idp_before_pw = min(h, hm) < pw
2866
2928
  self.args.ao_h_before_hm = h < hm
2867
2929
  self.args.ao_ipu_wins = ipu == 0
2868
- self.args.ao_have_pw = pw < 99
2930
+ self.args.ao_have_pw = pw < 99 or not self.args.have_idp_hdrs
2869
2931
 
2870
2932
  def load_idp_db(self, quiet=False) :
2871
2933
  # mutex me
@@ -3456,6 +3518,30 @@ class AuthSrv(object):
3456
3518
  self.log("generated config:\n\n" + "\n".join(ret))
3457
3519
 
3458
3520
 
3521
+ def n_du_who(s ) :
3522
+ if s == "all":
3523
+ return 9
3524
+ if s == "auth":
3525
+ return 7
3526
+ if s == "w":
3527
+ return 5
3528
+ if s == "rw":
3529
+ return 4
3530
+ if s == "a":
3531
+ return 3
3532
+ return 0
3533
+
3534
+
3535
+ def n_ver_who(s ) :
3536
+ if s == "all":
3537
+ return 9
3538
+ if s == "auth":
3539
+ return 6
3540
+ if s == "a":
3541
+ return 3
3542
+ return 0
3543
+
3544
+
3459
3545
  def split_cfg_ln(ln ) :
3460
3546
  # "a, b, c: 3" => {a:true, b:true, c:3}
3461
3547
  ret = {}
@@ -3488,7 +3574,9 @@ def expand_config_file(
3488
3574
 
3489
3575
  if os.path.isdir(fp):
3490
3576
  names = list(sorted(os.listdir(fp)))
3491
- cnames = [x for x in names if x.lower().endswith(".conf")]
3577
+ cnames = [
3578
+ x for x in names if x.lower().endswith(".conf") and not x.startswith(".")
3579
+ ]
3492
3580
  if not cnames:
3493
3581
  t = "warning: tried to read config-files from folder '%s' but it does not contain any "
3494
3582
  if names:
@@ -2,15 +2,17 @@
2
2
  from __future__ import print_function, unicode_literals
3
3
 
4
4
  import os
5
+ import time
5
6
 
6
7
  from ..util import SYMTIME, fsdec, fsenc
7
8
  from . import path as path
8
9
 
9
10
  MKD_755 = {"chmod_d": 0o755}
10
11
  MKD_700 = {"chmod_d": 0o700}
12
+ UTIME_CLAMPS = ((max, -2147483647), (max, 1), (min, 4294967294), (min, 2147483646))
11
13
 
12
- _ = (path, MKD_755, MKD_700)
13
- __all__ = ["path", "MKD_755", "MKD_700"]
14
+ _ = (path, MKD_755, MKD_700, UTIME_CLAMPS)
15
+ __all__ = ["path", "MKD_755", "MKD_700", "UTIME_CLAMPS"]
14
16
 
15
17
  # grep -hRiE '(^|[^a-zA-Z_\.-])os\.' . | gsed -r 's/ /\n/g;s/\(/(\n/g' | grep -hRiE '(^|[^a-zA-Z_\.-])os\.' | sort | uniq -c
16
18
  # printf 'os\.(%s)' "$(grep ^def bos/__init__.py | gsed -r 's/^def //;s/\(.*//' | tr '\n' '|' | gsed -r 's/.$//')"
@@ -96,6 +98,40 @@ def utime(
96
98
  return os.utime(fsenc(p), times)
97
99
 
98
100
 
101
+ def utime_c(
102
+ log , p , ts , follow_symlinks = True, throw = False
103
+ ) :
104
+ clamp = 0
105
+ ov = ts
106
+ bp = fsenc(p)
107
+ now = int(time.time())
108
+ while True:
109
+ try:
110
+ if SYMTIME:
111
+ os.utime(bp, (now, ts), follow_symlinks=follow_symlinks)
112
+ else:
113
+ os.utime(bp, (now, ts))
114
+ if clamp:
115
+ t = "filesystem rejected utime(%r); clamped %s to %s"
116
+ log(t % (p, ov, ts))
117
+ return ts
118
+ except Exception as ex:
119
+ pv = ts
120
+ while clamp < len(UTIME_CLAMPS):
121
+ fun, cv = UTIME_CLAMPS[clamp]
122
+ ts = fun(ts, cv)
123
+ clamp += 1
124
+ if ts != pv:
125
+ break
126
+ if clamp >= len(UTIME_CLAMPS):
127
+ if throw:
128
+ raise
129
+ else:
130
+ t = "could not utime(%r) to %s; %s, %r"
131
+ log(t % (p, ov, ex, ex))
132
+ return None
133
+
134
+
99
135
  if hasattr(os, "lstat"):
100
136
 
101
137
  def lstat(p ) :
@@ -84,6 +84,7 @@ def vf_vmap() :
84
84
  "chmod_d",
85
85
  "chmod_f",
86
86
  "dbd",
87
+ "du_who",
87
88
  "forget_ip",
88
89
  "hsortn",
89
90
  "html_head",
@@ -107,6 +108,7 @@ def vf_vmap() :
107
108
  "put_name",
108
109
  "mv_retry",
109
110
  "rm_retry",
111
+ "shr_who",
110
112
  "sort",
111
113
  "tail_fd",
112
114
  "tail_rate",
@@ -296,6 +298,7 @@ flagcats = {
296
298
  "html_head=TXT": "includes TXT in the <head>, or @PATH for file at PATH",
297
299
  "tcolor=#fc0": "theme color (a hint for webbrowsers, discord, etc.)",
298
300
  "nodirsz": "don't show total folder size",
301
+ "du_who=all": "show disk-usage info to everyone",
299
302
  "robots": "allows indexing by search engines (default)",
300
303
  "norobots": "kindly asks search engines to leave",
301
304
  "unlistcr": "don't list read-access in controlpanel",
@@ -348,6 +351,7 @@ flagcats = {
348
351
  "dky": 'allow seeing files (not folders) inside a specific folder\nwith "g" perm, and does not require a valid dirkey to do so',
349
352
  "rss": "allow '?rss' URL suffix (experimental)",
350
353
  "rmagic": "expensive analysis for mimetype accuracy",
354
+ "shr_who=auth": "who can create shares? no/auth/a",
351
355
  "unp_who=2": "unpost only if same... 1=ip+name, 2=ip, 3=name",
352
356
  "ups_who=2": "restrict viewing the list of recent uploads",
353
357
  "zip_who=2": "restrict access to download-as-zip/tar",
@@ -148,10 +148,6 @@ class FtpFs(AbstractedFS):
148
148
  self.cwd = "/" # pyftpdlib convention of leading slash
149
149
  self.root = "/var/lib/empty"
150
150
 
151
- self.can_read = self.can_write = self.can_move = False
152
- self.can_delete = self.can_get = self.can_upget = False
153
- self.can_admin = self.can_dot = False
154
-
155
151
  self.listdirinfo = self.listdir
156
152
  self.chdir(".")
157
153
 
@@ -214,7 +210,7 @@ class FtpFs(AbstractedFS):
214
210
  m = False,
215
211
  d = False,
216
212
  ) :
217
- return self.v2a(os.path.join(self.cwd, vpath), r, w, m, d)
213
+ return self.v2a(join(self.cwd, vpath), r, w, m, d)
218
214
 
219
215
  def ftp2fs(self, ftppath ) :
220
216
  # return self.v2a(ftppath)
@@ -293,16 +289,6 @@ class FtpFs(AbstractedFS):
293
289
  avfs = vfs
294
290
 
295
291
  self.cwd = nwd
296
- (
297
- self.can_read,
298
- self.can_write,
299
- self.can_move,
300
- self.can_delete,
301
- self.can_get,
302
- self.can_upget,
303
- self.can_admin,
304
- self.can_dot,
305
- ) = avfs.can_access("", self.h.uname)
306
292
 
307
293
  def mkdir(self, path ) :
308
294
  ap, vfs, _ = self.rv2a(path, w=True)
@@ -325,7 +311,7 @@ class FtpFs(AbstractedFS):
325
311
  vfs_ls = [x[0] for x in vfs_ls1]
326
312
  vfs_ls.extend(vfs_virt.keys())
327
313
 
328
- if not self.can_dot:
314
+ if self.uname not in vfs.axs.udot:
329
315
  vfs_ls = exclude_dotfiles(vfs_ls)
330
316
 
331
317
  vfs_ls.sort()
@@ -373,9 +359,6 @@ class FtpFs(AbstractedFS):
373
359
  raise FSE(str(ex))
374
360
 
375
361
  def rename(self, src , dst ) :
376
- if not self.can_move:
377
- raise FSE("Not allowed for user " + self.h.uname)
378
-
379
362
  if self.args.no_mv:
380
363
  raise FSE("The rename/move feature is disabled in server config")
381
364
 
@@ -405,12 +388,8 @@ class FtpFs(AbstractedFS):
405
388
  return st
406
389
 
407
390
  def utime(self, path , timeval ) :
408
- try:
409
- ap = self.rv2a(path, w=True)[0]
410
- return bos.utime(ap, (int(time.time()), int(timeval)))
411
- except Exception as ex:
412
- logging.error("ftp.utime: %s, %r", ex, ex)
413
- raise
391
+ ap = self.rv2a(path, w=True)[0]
392
+ bos.utime_c(logging.warning, ap, int(timeval), False)
414
393
 
415
394
  def lstat(self, path ) :
416
395
  ap = self.rv2a(path)[0]