copyparty 1.19.16__tar.gz → 1.19.17__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 (144) hide show
  1. {copyparty-1.19.16 → copyparty-1.19.17}/PKG-INFO +10 -2
  2. {copyparty-1.19.16 → copyparty-1.19.17}/README.md +9 -1
  3. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/__init__.py +19 -0
  4. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/__main__.py +31 -9
  5. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/__version__.py +2 -2
  6. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/authsrv.py +31 -6
  7. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/bos/bos.py +5 -1
  8. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/cfg.py +20 -0
  9. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/ftpd.py +5 -3
  10. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/httpcli.py +105 -20
  11. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/smbd.py +1 -1
  12. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/svchub.py +3 -0
  13. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/tftpd.py +1 -1
  14. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/up2k.py +39 -15
  15. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/util.py +15 -5
  16. copyparty-1.19.17/copyparty/web/baguettebox.js.gz +0 -0
  17. copyparty-1.19.17/copyparty/web/browser.css.gz +0 -0
  18. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/web/browser.html +3 -0
  19. copyparty-1.19.17/copyparty/web/browser.js.gz +0 -0
  20. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/web/splash.html +3 -0
  21. copyparty-1.19.17/copyparty/web/splash.js.gz +0 -0
  22. copyparty-1.19.17/copyparty/web/tl/chi.js.gz +0 -0
  23. copyparty-1.19.17/copyparty/web/tl/cze.js.gz +0 -0
  24. copyparty-1.19.17/copyparty/web/tl/deu.js.gz +0 -0
  25. copyparty-1.19.17/copyparty/web/tl/epo.js.gz +0 -0
  26. copyparty-1.19.17/copyparty/web/tl/fin.js.gz +0 -0
  27. copyparty-1.19.17/copyparty/web/tl/fra.js.gz +0 -0
  28. copyparty-1.19.17/copyparty/web/tl/grc.js.gz +0 -0
  29. copyparty-1.19.17/copyparty/web/tl/ita.js.gz +0 -0
  30. copyparty-1.19.17/copyparty/web/tl/kor.js.gz +0 -0
  31. copyparty-1.19.17/copyparty/web/tl/nld.js.gz +0 -0
  32. copyparty-1.19.17/copyparty/web/tl/nno.js.gz +0 -0
  33. copyparty-1.19.17/copyparty/web/tl/nor.js.gz +0 -0
  34. copyparty-1.19.17/copyparty/web/tl/pol.js.gz +0 -0
  35. copyparty-1.19.17/copyparty/web/tl/por.js.gz +0 -0
  36. copyparty-1.19.17/copyparty/web/tl/rus.js.gz +0 -0
  37. copyparty-1.19.17/copyparty/web/tl/spa.js.gz +0 -0
  38. copyparty-1.19.17/copyparty/web/tl/swe.js.gz +0 -0
  39. copyparty-1.19.17/copyparty/web/tl/tur.js.gz +0 -0
  40. copyparty-1.19.17/copyparty/web/tl/ukr.js.gz +0 -0
  41. copyparty-1.19.17/copyparty/web/util.js.gz +0 -0
  42. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty.egg-info/PKG-INFO +10 -2
  43. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty.egg-info/SOURCES.txt +20 -1
  44. {copyparty-1.19.16 → copyparty-1.19.17}/pyproject.toml +2 -0
  45. copyparty-1.19.16/copyparty/web/baguettebox.js.gz +0 -0
  46. copyparty-1.19.16/copyparty/web/browser.css.gz +0 -0
  47. copyparty-1.19.16/copyparty/web/browser.js.gz +0 -0
  48. copyparty-1.19.16/copyparty/web/splash.js.gz +0 -0
  49. copyparty-1.19.16/copyparty/web/util.js.gz +0 -0
  50. {copyparty-1.19.16 → copyparty-1.19.17}/LICENSE +0 -0
  51. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/bos/__init__.py +0 -0
  52. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/bos/path.py +0 -0
  53. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/broker_mp.py +0 -0
  54. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/broker_mpw.py +0 -0
  55. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/broker_thr.py +0 -0
  56. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/broker_util.py +0 -0
  57. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/cert.py +0 -0
  58. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/dxml.py +0 -0
  59. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/fsutil.py +0 -0
  60. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/httpconn.py +0 -0
  61. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/httpsrv.py +0 -0
  62. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/ico.py +0 -0
  63. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/mdns.py +0 -0
  64. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/metrics.py +0 -0
  65. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/mtag.py +2 -2
  66. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/multicast.py +0 -0
  67. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/pwhash.py +0 -0
  68. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/qrkode.py +0 -0
  69. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/res/COPYING.txt +0 -0
  70. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/res/__init__.py +0 -0
  71. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/res/insecure.pem +0 -0
  72. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/ssdp.py +0 -0
  73. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/star.py +0 -0
  74. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/stolen/__init__.py +0 -0
  75. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/stolen/dnslib/__init__.py +0 -0
  76. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/stolen/dnslib/bimap.py +0 -0
  77. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/stolen/dnslib/bit.py +0 -0
  78. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/stolen/dnslib/buffer.py +0 -0
  79. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/stolen/dnslib/dns.py +0 -0
  80. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/stolen/dnslib/label.py +0 -0
  81. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/stolen/dnslib/lex.py +0 -0
  82. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/stolen/dnslib/ranges.py +0 -0
  83. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/stolen/ifaddr/__init__.py +0 -0
  84. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/stolen/ifaddr/_posix.py +0 -0
  85. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/stolen/ifaddr/_shared.py +0 -0
  86. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/stolen/ifaddr/_win32.py +0 -0
  87. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/stolen/qrcodegen.py +0 -0
  88. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/stolen/surrogateescape.py +0 -0
  89. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/sutil.py +0 -0
  90. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/szip.py +0 -0
  91. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/tcpsrv.py +0 -0
  92. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/th_cli.py +0 -0
  93. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/th_srv.py +0 -0
  94. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/u2idx.py +0 -0
  95. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/web/a/__init__.py +0 -0
  96. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/web/a/partyfuse.py +0 -0
  97. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/web/a/u2c.py +0 -0
  98. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/web/a/webdav-cfg.bat +0 -0
  99. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/web/browser2.html +0 -0
  100. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/web/cf.html +0 -0
  101. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/web/dbg-audio.js.gz +0 -0
  102. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/web/deps/__init__.py +0 -0
  103. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/web/deps/busy.mp3.gz +0 -0
  104. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/web/deps/easymde.css.gz +0 -0
  105. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/web/deps/easymde.js.gz +0 -0
  106. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/web/deps/fuse.py +0 -0
  107. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/web/deps/marked.js.gz +0 -0
  108. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/web/deps/mini-fa.css.gz +0 -0
  109. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/web/deps/mini-fa.woff +0 -0
  110. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/web/deps/prism.css.gz +0 -0
  111. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/web/deps/prism.js.gz +0 -0
  112. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/web/deps/prismd.css.gz +0 -0
  113. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/web/deps/scp.woff2 +0 -0
  114. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/web/deps/sha512.ac.js.gz +0 -0
  115. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/web/deps/sha512.hw.js.gz +0 -0
  116. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/web/idp.html +0 -0
  117. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/web/md.css.gz +0 -0
  118. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/web/md.html +0 -0
  119. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/web/md.js.gz +0 -0
  120. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/web/md2.css.gz +0 -0
  121. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/web/md2.js.gz +0 -0
  122. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/web/mde.css.gz +0 -0
  123. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/web/mde.html +0 -0
  124. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/web/mde.js.gz +0 -0
  125. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/web/msg.css.gz +0 -0
  126. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/web/msg.html +0 -0
  127. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/web/opds.xml +0 -0
  128. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/web/rups.css.gz +0 -0
  129. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/web/rups.html +0 -0
  130. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/web/rups.js.gz +0 -0
  131. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/web/shares.css.gz +0 -0
  132. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/web/shares.html +0 -0
  133. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/web/shares.js.gz +0 -0
  134. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/web/splash.css.gz +0 -0
  135. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/web/svcs.html +0 -0
  136. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/web/svcs.js.gz +0 -0
  137. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/web/ui.css.gz +0 -0
  138. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/web/up2k.js.gz +0 -0
  139. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty/web/w.hash.js.gz +0 -0
  140. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty.egg-info/dependency_links.txt +0 -0
  141. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty.egg-info/entry_points.txt +0 -0
  142. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty.egg-info/requires.txt +0 -0
  143. {copyparty-1.19.16 → copyparty-1.19.17}/copyparty.egg-info/top_level.txt +0 -0
  144. {copyparty-1.19.16 → copyparty-1.19.17}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: copyparty
3
- Version: 1.19.16
3
+ Version: 1.19.17
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
@@ -588,6 +588,9 @@ examples:
588
588
 
589
589
  if you want to grant access to all users who are logged in, the group `acct` will always contain all known users, so for example `-v /mnt/music:music:r,@acct`
590
590
 
591
+ * to do the opposite, granting access to everyone who is NOT logged in. `*,-@acct` does the trick, for example `-v /srv/welcome:welcome:r,*,-@acct`
592
+ * single users can also be subtracted from a group: `@admins,-james`
593
+
591
594
  anyone trying to bruteforce a password gets banned according to `--ban-pw`; default is 24h ban for 9 failed attempts in 1 hour
592
595
 
593
596
  and if you want to use config files instead of commandline args (good!) then here's the same examples as a configfile; save it as `foobar.conf` and use it like this: `python copyparty-sfx.py -c foobar.conf`
@@ -1558,7 +1561,12 @@ tweaking the ui
1558
1561
  * to sort in music order (album, track, artist, title) with filename as fallback, you could `--sort tags/Circle,tags/.tn,tags/Artist,tags/Title,href`
1559
1562
  * to sort by upload date, first enable showing the upload date in the listing with `-e2d -mte +.up_at` and then `--sort tags/.up_at`
1560
1563
 
1561
- see [./docs/rice](./docs/rice) for more, including how to add stuff (css/`<meta>`/...) to the html `<head>` tag, or to add your own translation
1564
+ see [./docs/rice](./docs/rice) for more, including:
1565
+ * how to [hide ui-elements](./docs/rice/README.md#hide-ui-elements)
1566
+ * [custom fonts](./docs/rice/README.md#custom-fonts)
1567
+ * [custom loading-spinner](./docs/rice/README.md#boring-loader-spinner)
1568
+ * adding stuff (css/`<meta>`/...) [to the html `<head>` tag](./docs/rice/README.md#head)
1569
+ * [adding your own translation](./docs/rice/README.md#translations)
1562
1570
 
1563
1571
 
1564
1572
  ## opengraph
@@ -523,6 +523,9 @@ examples:
523
523
 
524
524
  if you want to grant access to all users who are logged in, the group `acct` will always contain all known users, so for example `-v /mnt/music:music:r,@acct`
525
525
 
526
+ * to do the opposite, granting access to everyone who is NOT logged in. `*,-@acct` does the trick, for example `-v /srv/welcome:welcome:r,*,-@acct`
527
+ * single users can also be subtracted from a group: `@admins,-james`
528
+
526
529
  anyone trying to bruteforce a password gets banned according to `--ban-pw`; default is 24h ban for 9 failed attempts in 1 hour
527
530
 
528
531
  and if you want to use config files instead of commandline args (good!) then here's the same examples as a configfile; save it as `foobar.conf` and use it like this: `python copyparty-sfx.py -c foobar.conf`
@@ -1493,7 +1496,12 @@ tweaking the ui
1493
1496
  * to sort in music order (album, track, artist, title) with filename as fallback, you could `--sort tags/Circle,tags/.tn,tags/Artist,tags/Title,href`
1494
1497
  * to sort by upload date, first enable showing the upload date in the listing with `-e2d -mte +.up_at` and then `--sort tags/.up_at`
1495
1498
 
1496
- see [./docs/rice](./docs/rice) for more, including how to add stuff (css/`<meta>`/...) to the html `<head>` tag, or to add your own translation
1499
+ see [./docs/rice](./docs/rice) for more, including:
1500
+ * how to [hide ui-elements](./docs/rice/README.md#hide-ui-elements)
1501
+ * [custom fonts](./docs/rice/README.md#custom-fonts)
1502
+ * [custom loading-spinner](./docs/rice/README.md#boring-loader-spinner)
1503
+ * adding stuff (css/`<meta>`/...) [to the html `<head>` tag](./docs/rice/README.md#head)
1504
+ * [adding your own translation](./docs/rice/README.md#translations)
1497
1505
 
1498
1506
 
1499
1507
  ## opengraph
@@ -97,6 +97,25 @@ web/splash.html
97
97
  web/splash.js
98
98
  web/svcs.html
99
99
  web/svcs.js
100
+ web/tl/chi.js
101
+ web/tl/cze.js
102
+ web/tl/deu.js
103
+ web/tl/epo.js
104
+ web/tl/fin.js
105
+ web/tl/fra.js
106
+ web/tl/grc.js
107
+ web/tl/ita.js
108
+ web/tl/kor.js
109
+ web/tl/nld.js
110
+ web/tl/nno.js
111
+ web/tl/nor.js
112
+ web/tl/pol.js
113
+ web/tl/por.js
114
+ web/tl/rus.js
115
+ web/tl/spa.js
116
+ web/tl/swe.js
117
+ web/tl/tur.js
118
+ web/tl/ukr.js
100
119
  web/ui.css
101
120
  web/up2k.js
102
121
  web/util.js
@@ -641,8 +641,11 @@ def get_sects():
641
641
  if no accounts or volumes are configured,
642
642
  current folder will be read/write for everyone
643
643
 
644
- the group @acct will always have every user with an account
645
- (the name of that group can be changed with --grp-all)
644
+ the group \033[33m@acct\033[0m will always have every user with an account
645
+ (the name of that group can be changed with \033[32m--grp-all\033[0m)
646
+
647
+ to hide a volume from authenticated users, specify \033[33m*,-@acct\033[0m
648
+ to subtract \033[33m@acct\033[0m from \033[33m*\033[0m (can subtract users from groups too)
646
649
 
647
650
  consider the config file for more flexible account/volume management,
648
651
  including dynamic reload at runtime (and being more readable w)
@@ -664,12 +667,12 @@ def get_sects():
664
667
 
665
668
  send the password in the '\033[36mPW\033[0m' http-header:
666
669
  \033[36mPW: \033[35mhunter2\033[0m
667
- or if you have \033[33m--accounts\033[0m enabled,
670
+ or if you have \033[33m--usernames\033[0m enabled,
668
671
  \033[36mPW: \033[35med:hunter2\033[0m
669
672
 
670
673
  send the password in the URL itself:
671
674
  \033[36mhttp://127.0.0.1:3923/\033[35m?pw=hunter2\033[0m
672
- or if you have \033[33m--accounts\033[0m enabled,
675
+ or if you have \033[33m--usernames\033[0m enabled,
673
676
  \033[36mhttp://127.0.0.1:3923/\033[35m?pw=ed:hunter2\033[0m
674
677
 
675
678
  use basic-authentication:
@@ -1166,6 +1169,7 @@ def add_general(ap, nc, srvname):
1166
1169
  ap2.add_argument("--rmagic", action="store_true", help="do expensive analysis to improve accuracy of returned mimetypes; will make file-downloads, rss, and webdav slower (volflag=rmagic)")
1167
1170
  ap2.add_argument("--license", action="store_true", help="show licenses and exit")
1168
1171
  ap2.add_argument("--version", action="store_true", help="show versions and exit")
1172
+ ap2.add_argument("--versionb", action="store_true", help="show version and exit")
1169
1173
 
1170
1174
 
1171
1175
  def add_qr(ap, tty):
@@ -1233,6 +1237,7 @@ def add_upload(ap):
1233
1237
  ap2.add_argument("--hardlink-only", action="store_true", help="do not fallback to symlinks when a hardlink cannot be made (volflag=hardlinkonly)")
1234
1238
  ap2.add_argument("--reflink", action="store_true", help="enable reflink-based dedup; will fallback on full copies when that is impossible (non-CoW filesystem) (volflag=reflink)")
1235
1239
  ap2.add_argument("--no-dupe", action="store_true", help="reject duplicate files during upload; only matches within the same volume (volflag=nodupe)")
1240
+ ap2.add_argument("--no-dupe-m", action="store_true", help="also reject dupes when moving a file into another volume (volflag=nodupem)")
1236
1241
  ap2.add_argument("--no-clone", action="store_true", help="do not use existing data on disk to satisfy dupe uploads; reduces server HDD reads in exchange for much more network load (volflag=noclone)")
1237
1242
  ap2.add_argument("--no-snap", action="store_true", help="disable snapshots -- forget unfinished uploads on shutdown; don't create .hist/up2k.snap files -- abandoned/interrupted uploads must be cleaned up manually")
1238
1243
  ap2.add_argument("--snap-wri", metavar="SEC", type=int, default=300, help="write upload state to ./hist/up2k.snap every \033[33mSEC\033[0m seconds; allows resuming incomplete uploads after a server crash")
@@ -1520,6 +1525,7 @@ def add_optouts(ap):
1520
1525
  ap2.add_argument("--no-pipe", action="store_true", help="disable race-the-beam (lockstep download of files which are currently being uploaded) (volflag=nopipe)")
1521
1526
  ap2.add_argument("--no-tail", action="store_true", help="disable streaming a growing files with ?tail (volflag=notail)")
1522
1527
  ap2.add_argument("--no-db-ip", action="store_true", help="do not write uploader-IP into the database; will also disable unpost, you may want \033[32m--forget-ip\033[0m instead (volflag=no_db_ip)")
1528
+ ap2.add_argument("--no-zls", action="store_true", help="disable browsing the contents of zip/cbz files, does not affect thumbnails")
1523
1529
 
1524
1530
 
1525
1531
  def add_safety(ap):
@@ -1538,6 +1544,7 @@ def add_safety(ap):
1538
1544
  ap2.add_argument("--force-js", action="store_true", help="don't send folder listings as HTML, force clients to use the embedded json instead -- slight protection against misbehaving search engines which ignore \033[33m--no-robots\033[0m")
1539
1545
  ap2.add_argument("--no-robots", action="store_true", help="adds http and html headers asking search engines to not index anything (volflag=norobots)")
1540
1546
  ap2.add_argument("--logout", metavar="H", type=float, default=8086.0, help="logout clients after \033[33mH\033[0m hours of inactivity; [\033[32m0.0028\033[0m]=10sec, [\033[32m0.1\033[0m]=6min, [\033[32m24\033[0m]=day, [\033[32m168\033[0m]=week, [\033[32m720\033[0m]=month, [\033[32m8760\033[0m]=year)")
1547
+ ap2.add_argument("--dont-ban", metavar="TXT", type=u, default="no", help="anyone at this accesslevel or above will not get banned: [\033[32mav\033[0m]=admin-in-volume, [\033[32maa\033[0m]=has-admin-anywhere, [\033[32mrw\033[0m]=read-write, [\033[32mauth\033[0m]=authenticated, [\033[32many\033[0m]=disable-all-bans, [\033[32mno\033[0m]=anyone-can-get-banned")
1541
1548
  ap2.add_argument("--ban-pw", metavar="N,W,B", type=u, default="9,60,1440", help="more than \033[33mN\033[0m wrong passwords in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes; disable with [\033[32mno\033[0m]")
1542
1549
  ap2.add_argument("--ban-pwc", metavar="N,W,B", type=u, default="5,60,1440", help="more than \033[33mN\033[0m password-changes in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes; disable with [\033[32mno\033[0m]")
1543
1550
  ap2.add_argument("--ban-404", metavar="N,W,B", type=u, default="50,60,1440", help="hitting more than \033[33mN\033[0m 404's in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes; only affects users who cannot see directory listings because their access is either g/G/h")
@@ -1602,12 +1609,14 @@ def add_admin(ap):
1602
1609
  ap2 = ap.add_argument_group("admin panel options")
1603
1610
  ap2.add_argument("--no-reload", action="store_true", help="disable ?reload=cfg (reload users/volumes/volflags from config file)")
1604
1611
  ap2.add_argument("--no-rescan", action="store_true", help="disable ?scan (volume reindexing)")
1605
- ap2.add_argument("--no-stack", action="store_true", help="disable ?stack (list all stacks)")
1612
+ ap2.add_argument("--no-stack", action="store_true", help="disable ?stack (list all stacks); same as --stack-who=no")
1606
1613
  ap2.add_argument("--no-ups-page", action="store_true", help="disable ?ru (list of recent uploads)")
1607
1614
  ap2.add_argument("--no-up-list", action="store_true", help="don't show list of incoming files in controlpanel")
1608
1615
  ap2.add_argument("--dl-list", metavar="LVL", type=int, default=2, help="who can see active downloads in the controlpanel? [\033[32m0\033[0m]=nobody, [\033[32m1\033[0m]=admins, [\033[32m2\033[0m]=everyone")
1609
1616
  ap2.add_argument("--ups-who", metavar="LVL", type=int, default=2, help="who can see recent uploads on the ?ru page? [\033[32m0\033[0m]=nobody, [\033[32m1\033[0m]=admins, [\033[32m2\033[0m]=everyone (volflag=ups_who)")
1610
1617
  ap2.add_argument("--ups-when", action="store_true", help="let everyone see upload timestamps on the ?ru page, not just admins")
1618
+ ap2.add_argument("--stack-who", metavar="LVL", type=u, default="a", help="who can see the ?stack page (list of threads)? [\033[32mno\033[0m]=nobody, [\033[32ma\033[0m]=admins, [\033[32mrw\033[0m]=read+write, [\033[32mall\033[0m]=everyone")
1619
+ ap2.add_argument("--stack-v", action="store_true", help="verbose ?stack")
1611
1620
 
1612
1621
 
1613
1622
  def add_thumbnail(ap):
@@ -1805,6 +1814,15 @@ def add_ui(ap, retry ):
1805
1814
  ap2.add_argument("--lg-sba", metavar="TXT", type=u, default="", help="the value of the iframe 'allow' attribute for prologue/epilogue docs (volflag=lg_sba); see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Permissions-Policy#iframes")
1806
1815
  ap2.add_argument("--no-sb-md", action="store_true", help="don't sandbox README/PREADME.md documents (volflags: no_sb_md | sb_md)")
1807
1816
  ap2.add_argument("--no-sb-lg", action="store_true", help="don't sandbox prologue/epilogue docs (volflags: no_sb_lg | sb_lg); enables non-js support")
1817
+ ap2.add_argument("--ui-nombar", action="store_true", help="hide top-menu in the UI (volflag=ui_nombar)")
1818
+ ap2.add_argument("--ui-noacci", action="store_true", help="hide account-info in the UI (volflag=ui_noacci)")
1819
+ ap2.add_argument("--ui-nosrvi", action="store_true", help="hide server-info in the UI (volflag=ui_nosrvi)")
1820
+ ap2.add_argument("--ui-nonav", action="store_true", help="hide navpane+breadcrumbs (volflag=ui_nonav)")
1821
+ ap2.add_argument("--ui-notree", action="store_true", help="hide navpane in the UI (volflag=ui_nonav)")
1822
+ ap2.add_argument("--ui-nocpla", action="store_true", help="hide cpanel-link in the UI (volflag=ui_nocpla)")
1823
+ ap2.add_argument("--ui-nolbar", action="store_true", help="hide link-bar in the UI (volflag=ui_nolbar)")
1824
+ ap2.add_argument("--ui-noctxb", action="store_true", help="hide context-buttons in the UI (volflag=ui_noctxb)")
1825
+ ap2.add_argument("--ui-norepl", action="store_true", help="hide repl-button in the UI (volflag=ui_norepl)")
1808
1826
  ap2.add_argument("--have-unlistc", action="store_true", help=argparse.SUPPRESS)
1809
1827
 
1810
1828
 
@@ -1932,15 +1950,19 @@ def run_argparse(
1932
1950
 
1933
1951
 
1934
1952
  def main(argv = None) :
1953
+ if argv is None:
1954
+ argv = sys.argv
1955
+
1956
+ if "--versionb" in argv:
1957
+ print(S_VERSION)
1958
+ sys.exit(0)
1959
+
1935
1960
  time.strptime("19970815", "%Y%m%d") # python#7980
1936
1961
  if WINDOWS:
1937
1962
  os.system("rem") # enables colors
1938
1963
 
1939
1964
  init_E(E)
1940
1965
 
1941
- if argv is None:
1942
- argv = sys.argv
1943
-
1944
1966
  f = '\033[36mcopyparty v{} "\033[35m{}\033[36m" ({})\n{}\033[0;36m\n sqlite {} | jinja {} | pyftpd {} | tftp {}\n\033[0m'
1945
1967
  f = f.format(
1946
1968
  S_VERSION,
@@ -2067,7 +2089,7 @@ def main(argv = None) :
2067
2089
 
2068
2090
  # propagate implications
2069
2091
  for k1, k2 in IMPLICATIONS:
2070
- if getattr(al, k1):
2092
+ if getattr(al, k1, None):
2071
2093
  setattr(al, k2, True)
2072
2094
 
2073
2095
  # propagate unplications
@@ -1,8 +1,8 @@
1
1
  # coding: utf-8
2
2
 
3
- VERSION = (1, 19, 16)
3
+ VERSION = (1, 19, 17)
4
4
  CODENAME = "usernames"
5
- BUILD_DT = (2025, 10, 5)
5
+ BUILD_DT = (2025, 10, 17)
6
6
 
7
7
  S_VERSION = ".".join(map(str, VERSION))
8
8
  S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)
@@ -1961,9 +1961,18 @@ class AuthSrv(object):
1961
1961
  axs_key = "u" + perm
1962
1962
  for vp, vol in vfs.all_vols.items():
1963
1963
  zx = getattr(vol.axs, axs_key)
1964
- if "*" in zx:
1964
+ if "*" in zx and "-@acct" not in zx:
1965
1965
  for usr in unames:
1966
1966
  zx.add(usr)
1967
+ for zs in list(zx):
1968
+ if zs.startswith("-"):
1969
+ zx.discard(zs)
1970
+ zs = zs[1:]
1971
+ zx.discard(zs)
1972
+ if zs.startswith("@"):
1973
+ zs = zs[1:]
1974
+ for zs in grps.get(zs) or []:
1975
+ zx.discard(zs)
1967
1976
 
1968
1977
  # aread,... = dict[uname, list[volnames] or []]
1969
1978
  umap = {x: [] for x in unames}
@@ -2577,6 +2586,15 @@ class AuthSrv(object):
2577
2586
  for x in drop:
2578
2587
  vol.flags.pop(x)
2579
2588
 
2589
+ zi = vol.flags.get("lifetime") or 0
2590
+ zi2 = time.time() // (86400 * 365)
2591
+ zi3 = zi2 * 86400 * 365
2592
+ if zi < 0 or zi > zi3:
2593
+ t = "the lifetime of volume [/%s] (%d) exceeds max value (%d years; %d)"
2594
+ t = t % (vol.vpath, zi, zi2, zi3)
2595
+ self.log(t, 1)
2596
+ raise Exception(t)
2597
+
2580
2598
  # verify tags mentioned by -mt[mp] are used by -mte
2581
2599
  local_mtp = {}
2582
2600
  local_only_mtp = {}
@@ -2743,9 +2761,13 @@ class AuthSrv(object):
2743
2761
  ["uadmin", "uadmin"],
2744
2762
  ]:
2745
2763
  u = list(sorted(getattr(zv.axs, attr)))
2746
- u = ["*"] if "*" in u else u
2747
- u = ", ".join("\033[35meverybody\033[0m" if x == "*" else x for x in u)
2748
- u = u if u else "\033[36m--none--\033[0m"
2764
+ if u == ["*"] and acct:
2765
+ u = ["\033[35monly-anonymous\033[0m"]
2766
+ elif "*" in u:
2767
+ u = ["\033[35meverybody\033[0m"]
2768
+ if not u:
2769
+ u = ["\033[36m--none--\033[0m"]
2770
+ u = ", ".join(u)
2749
2771
  t += "\n| {}: {}".format(txt, u)
2750
2772
 
2751
2773
  if "e2d" in zv.flags:
@@ -2857,8 +2879,6 @@ class AuthSrv(object):
2857
2879
 
2858
2880
  if have_reflink:
2859
2881
  t = "WARNING: Reflink-based dedup was requested, but %s. This will not work; files will be full copies instead."
2860
- if sys.version_info < (3, 14):
2861
- self.log(t % "your python version is not new enough", 1)
2862
2882
  if not sys.platform.startswith("linux"):
2863
2883
  self.log(t % "your OS is not Linux", 1)
2864
2884
 
@@ -3016,6 +3036,7 @@ class AuthSrv(object):
3016
3036
  "have_shr": self.args.shr,
3017
3037
  "shr_who": vf["shr_who"],
3018
3038
  "have_zip": not self.args.no_zip,
3039
+ "have_zls": not self.args.no_zls,
3019
3040
  "have_mv": not self.args.no_mv,
3020
3041
  "have_del": not self.args.no_del,
3021
3042
  "have_unpost": int(self.args.unpost),
@@ -3052,6 +3073,10 @@ class AuthSrv(object):
3052
3073
  "lifetime": vn.js_ls["lifetime"],
3053
3074
  "u2sort": self.args.u2sort,
3054
3075
  }
3076
+ zs = "ui_noacci ui_nocpla ui_noctxb ui_nolbar ui_nombar ui_nonav ui_notree ui_norepl ui_nosrvi"
3077
+ for zs in zs.split():
3078
+ if vf.get(zs):
3079
+ js_htm[zs] = 1
3055
3080
  vn.js_htm = json_hesc(json.dumps(js_htm))
3056
3081
 
3057
3082
  vols = list(vfs.all_nodes.values())
@@ -99,7 +99,11 @@ def utime(
99
99
 
100
100
 
101
101
  def utime_c(
102
- log , p , ts , follow_symlinks = True, throw = False
102
+ log ,
103
+ p ,
104
+ ts ,
105
+ follow_symlinks = True,
106
+ throw = False,
103
107
  ) :
104
108
  clamp = 0
105
109
  ov = ts
@@ -19,6 +19,7 @@ def vf_bmap() :
19
19
  "no_clone": "noclone",
20
20
  "no_dirsz": "nodirsz",
21
21
  "no_dupe": "nodupe",
22
+ "no_dupe_m": "nodupem",
22
23
  "no_forget": "noforget",
23
24
  "no_pipe": "nopipe",
24
25
  "no_robots": "norobots",
@@ -58,6 +59,15 @@ def vf_bmap() :
58
59
  "rm_partial",
59
60
  "rmagic",
60
61
  "rss",
62
+ "ui_noacci",
63
+ "ui_nocpla",
64
+ "ui_nolbar",
65
+ "ui_nombar",
66
+ "ui_nonav",
67
+ "ui_notree",
68
+ "ui_norepl",
69
+ "ui_nosrvi",
70
+ "ui_noctxb",
61
71
  "wo_up_readme",
62
72
  "wram",
63
73
  "xdev",
@@ -189,6 +199,7 @@ flagcats = {
189
199
  "safededup": "verify on-disk data before using it for dedup",
190
200
  "noclone": "take dupe data from clients, even if available on HDD",
191
201
  "nodupe": "rejects existing files (instead of linking/cloning them)",
202
+ "nodupem": "rejects existing files during moves as well",
192
203
  "chmod_d=755": "unix-permission for new dirs/folders",
193
204
  "chmod_f=644": "unix-permission for new files",
194
205
  "uid=573": "change owner of new files/folders to unix-user 573",
@@ -325,6 +336,15 @@ flagcats = {
325
336
  "md_sba": "value of iframe allow-prop for markdown-sandbox",
326
337
  "lg_sba": "value of iframe allow-prop for *logue-sandbox",
327
338
  "nohtml": "return html and markdown as text/html",
339
+ "ui_noacci": "hide account-info in the UI",
340
+ "ui_nocpla": "hide cpanel-link in the UI",
341
+ "ui_nolbar": "hide link-bar in the UI",
342
+ "ui_nombar": "hide top-menu in the UI",
343
+ "ui_nonav": "hide navpane+breadcrumbs in the UI",
344
+ "ui_notree": "hide navpane in the UI",
345
+ "ui_norepl": "hide repl-button in the UI",
346
+ "ui_nosrvi": "hide server-info in the UI",
347
+ "ui_noctxb": "hide context-buttons in the UI",
328
348
  },
329
349
  "opengraph (discord embeds)": {
330
350
  "og": "enable OG (disables hotlinking)",
@@ -247,7 +247,7 @@ class FtpFs(AbstractedFS):
247
247
 
248
248
  if w and need_unlink:
249
249
  if td >= -1 and td <= self.args.ftp_wt:
250
- # within permitted timeframe; unlink and accept
250
+ # within permitted timeframe; allow overwrite or resume
251
251
  do_it = True
252
252
  elif self.args.no_del or self.args.ftp_no_ow:
253
253
  # file too old, or overwrite not allowed; reject
@@ -264,7 +264,9 @@ class FtpFs(AbstractedFS):
264
264
  if not do_it:
265
265
  raise FSE("File already exists")
266
266
 
267
- wunlink(self.log, ap, VF_CAREFUL)
267
+ # Don't unlink file for append mode
268
+ elif "a" not in mode:
269
+ wunlink(self.log, ap, VF_CAREFUL)
268
270
 
269
271
  ret = open(fsenc(ap), mode, self.args.iobuf)
270
272
  if w and "fperms" in vfs.flags:
@@ -505,7 +507,7 @@ class FtpHandler(FTPHandler):
505
507
  0,
506
508
  self.cli_ip,
507
509
  time.time(),
508
- "",
510
+ None,
509
511
  )
510
512
  t = hr.get("rejectmsg") or ""
511
513
  if t or not hr:
@@ -270,7 +270,7 @@ class HttpCli(object):
270
270
  tpl = self.conn.hsrv.j2[name]
271
271
  ka["r"] = self.args.SR if self.is_vproxied else ""
272
272
  ka["ts"] = self.conn.hsrv.cachebuster()
273
- ka["lang"] = self.args.lang
273
+ ka["lang"] = self.cookies.get("cplng") or self.args.lang
274
274
  ka["favico"] = self.args.favico
275
275
  ka["s_doctitle"] = self.args.doctitle
276
276
  ka["tcolor"] = self.vn.flags["tcolor"]
@@ -857,6 +857,16 @@ class HttpCli(object):
857
857
  return self.conn.iphash.s(self.ip)
858
858
 
859
859
  def cbonk(self, g , v , reason , descr ) :
860
+ cond = self.args.dont_ban
861
+ if (
862
+ cond == "any"
863
+ or (cond == "auth" and self.uname != "*")
864
+ or (cond == "aa" and self.avol)
865
+ or (cond == "av" and self.can_admin)
866
+ or (cond == "rw" and self.can_read and self.can_write)
867
+ ):
868
+ return False
869
+
860
870
  self.conn.hsrv.nsus += 1
861
871
  if not g.lim:
862
872
  return False
@@ -881,7 +891,7 @@ class HttpCli(object):
881
891
  0,
882
892
  self.ip,
883
893
  time.time(),
884
- reason,
894
+ [reason, reason],
885
895
  ):
886
896
  self.log("client banned: %s" % (descr,), 1)
887
897
  self.conn.hsrv.bans[ip] = bonk
@@ -1516,6 +1526,64 @@ class HttpCli(object):
1516
1526
  self.log("rss: %d hits, %d bytes" % (len(hits), len(bret)))
1517
1527
  return True
1518
1528
 
1529
+ def tx_zls(self, abspath) :
1530
+ if self.do_log:
1531
+ self.log("zls %s @%s" % (self.req, self.uname))
1532
+ if self.args.no_zls:
1533
+ raise Pebkac(405, "zip browsing is disabled in server config")
1534
+
1535
+ import zipfile
1536
+
1537
+ try:
1538
+ with zipfile.ZipFile(abspath, "r") as zf:
1539
+ filelist = [{"fn": f.filename} for f in zf.infolist()]
1540
+ ret = json.dumps(filelist).encode("utf-8", "replace")
1541
+ self.reply(ret, mime="application/json")
1542
+ return True
1543
+ except (zipfile.BadZipfile, RuntimeError):
1544
+ raise Pebkac(404, "requested file is not a valid zip file")
1545
+
1546
+ def tx_zget(self, abspath) :
1547
+ maxsz = 1024 * 1024 * 64
1548
+
1549
+ inner_path = self.uparam.get("zget")
1550
+ if not inner_path:
1551
+ raise Pebkac(405, "inner path is required")
1552
+ if self.do_log:
1553
+ self.log(
1554
+ "zget %s \033[35m%s\033[0m @%s" % (self.req, inner_path, self.uname)
1555
+ )
1556
+ if self.args.no_zls:
1557
+ raise Pebkac(405, "zip browsing is disabled in server config")
1558
+
1559
+ import zipfile
1560
+
1561
+ try:
1562
+ with zipfile.ZipFile(abspath, "r") as zf:
1563
+ zi = zf.getinfo(inner_path)
1564
+ if zi.file_size >= maxsz:
1565
+ raise Pebkac(404, "zip bomb defused")
1566
+ with zf.open(zi, "r") as fi:
1567
+ self.send_headers(length=zi.file_size, mime=guess_mime(inner_path))
1568
+
1569
+ sendfile_py(
1570
+ self.log,
1571
+ 0,
1572
+ zi.file_size,
1573
+ fi,
1574
+ self.s,
1575
+ self.args.s_wr_sz,
1576
+ self.args.s_wr_slp,
1577
+ not self.args.no_poll,
1578
+ {},
1579
+ "",
1580
+ )
1581
+ except KeyError:
1582
+ raise Pebkac(404, "no such file in archive")
1583
+ except (zipfile.BadZipfile, RuntimeError):
1584
+ raise Pebkac(404, "requested file is not a valid zip file")
1585
+ return True
1586
+
1519
1587
  def handle_propfind(self) :
1520
1588
  if self.do_log:
1521
1589
  self.log("PFIND %s @%s" % (self.req, self.uname))
@@ -2079,7 +2147,7 @@ class HttpCli(object):
2079
2147
  t = "urlform_raw %d @ %r\n %r\n"
2080
2148
  self.log(t % (len(orig), "/" + self.vpath, orig))
2081
2149
  try:
2082
- zb = unquote(buf.replace(b"+", b" "))
2150
+ zb = unquote(buf.replace(b"+", b" ").replace(b"&", b"\n"))
2083
2151
  plain = zb.decode("utf-8", "replace")
2084
2152
  if buf.startswith(b"msg="):
2085
2153
  plain = plain[4:]
@@ -2100,7 +2168,7 @@ class HttpCli(object):
2100
2168
  len(buf),
2101
2169
  self.ip,
2102
2170
  time.time(),
2103
- plain,
2171
+ [plain, orig],
2104
2172
  )
2105
2173
 
2106
2174
  t = "urlform_dec %d @ %r\n %r\n"
@@ -2259,7 +2327,7 @@ class HttpCli(object):
2259
2327
  remains,
2260
2328
  self.ip,
2261
2329
  at,
2262
- "",
2330
+ None,
2263
2331
  )
2264
2332
  t = hr.get("rejectmsg") or ""
2265
2333
  if t or not hr:
@@ -2394,7 +2462,7 @@ class HttpCli(object):
2394
2462
  post_sz,
2395
2463
  self.ip,
2396
2464
  at,
2397
- "",
2465
+ None,
2398
2466
  )
2399
2467
  t = hr.get("rejectmsg") or ""
2400
2468
  if t or not hr:
@@ -3226,7 +3294,7 @@ class HttpCli(object):
3226
3294
  0,
3227
3295
  self.ip,
3228
3296
  time.time(),
3229
- "",
3297
+ None,
3230
3298
  )
3231
3299
  t = hr.get("rejectmsg") or ""
3232
3300
  if t or not hr:
@@ -3398,7 +3466,7 @@ class HttpCli(object):
3398
3466
  0,
3399
3467
  self.ip,
3400
3468
  at,
3401
- "",
3469
+ None,
3402
3470
  )
3403
3471
  t = hr.get("rejectmsg") or ""
3404
3472
  if t or not hr:
@@ -3505,7 +3573,7 @@ class HttpCli(object):
3505
3573
  sz,
3506
3574
  self.ip,
3507
3575
  at,
3508
- "",
3576
+ None,
3509
3577
  )
3510
3578
  t = hr.get("rejectmsg") or ""
3511
3579
  if t or not hr:
@@ -3816,7 +3884,7 @@ class HttpCli(object):
3816
3884
  0,
3817
3885
  self.ip,
3818
3886
  time.time(),
3819
- "",
3887
+ None,
3820
3888
  )
3821
3889
  t = hr.get("rejectmsg") or ""
3822
3890
  if t or not hr:
@@ -3864,7 +3932,7 @@ class HttpCli(object):
3864
3932
  sz,
3865
3933
  self.ip,
3866
3934
  new_lastmod,
3867
- "",
3935
+ None,
3868
3936
  )
3869
3937
  t = hr.get("rejectmsg") or ""
3870
3938
  if t or not hr:
@@ -4998,7 +5066,7 @@ class HttpCli(object):
4998
5066
  "edit": "edit" in self.uparam,
4999
5067
  "title": html_escape(self.vpath, crlf=True),
5000
5068
  "lastmod": int(ts_md * 1000),
5001
- "lang": self.args.lang,
5069
+ "lang": self.cookies.get("cplng") or self.args.lang,
5002
5070
  "favico": self.args.favico,
5003
5071
  "have_emp": int(self.args.emp),
5004
5072
  "md_no_br": int(vn.flags.get("md_no_br") or 0),
@@ -5383,13 +5451,20 @@ class HttpCli(object):
5383
5451
  return self.redirect("", "?h", x.get(), "return to", False)
5384
5452
 
5385
5453
  def tx_stack(self) :
5386
- if not self.avol and not [x for x in self.wvol if x in self.rvol]:
5454
+ zs = self.args.stack_who
5455
+ if zs == "all" or (
5456
+ (zs == "a" and self.avol)
5457
+ or (zs == "rw" and [x for x in self.wvol if x in self.rvol])
5458
+ ):
5459
+ pass
5460
+ else:
5387
5461
  raise Pebkac(403, "'stack' not allowed for user " + self.uname)
5388
5462
 
5389
- if self.args.no_stack:
5390
- raise Pebkac(403, "the stackdump feature is disabled in server config")
5391
-
5392
- ret = "<pre>{}\n{}".format(time.time(), html_escape(alltrace()))
5463
+ ret = html_escape(alltrace(self.args.stack_v))
5464
+ if self.args.stack_v:
5465
+ ret = "<pre>%s\n%s" % (time.time(), ret)
5466
+ else:
5467
+ ret = "<pre>%s" % (ret,)
5393
5468
  self.reply(ret.encode("utf-8"))
5394
5469
  return True
5395
5470
 
@@ -6456,14 +6531,23 @@ class HttpCli(object):
6456
6531
  ):
6457
6532
  return self.tx_md(vn, abspath)
6458
6533
 
6534
+ if "zls" in self.uparam:
6535
+ return self.tx_zls(abspath)
6536
+ if "zget" in self.uparam:
6537
+ return self.tx_zget(abspath)
6538
+
6459
6539
  if not add_og or not og_fn:
6460
- return self.tx_file(
6461
- abspath, None if st.st_size or "nopipe" in vn.flags else vn.realpath
6462
- )
6540
+ if st.st_size or "nopipe" in vn.flags:
6541
+ return self.tx_file(abspath, None)
6542
+ else:
6543
+ return self.tx_file(abspath, vn.get_dbv("")[0].realpath)
6463
6544
 
6464
6545
  elif is_dir and not self.can_read:
6465
6546
  if use_dirkey:
6466
6547
  is_dk = True
6548
+ elif self.can_get and "doc" in self.uparam:
6549
+ zs = vjoin(self.vpath, self.uparam["doc"]) + "?v"
6550
+ return self.redirect(zs, flavor="redirecting to", use302=True)
6467
6551
  elif not self.can_write:
6468
6552
  return self.tx_404(True)
6469
6553
 
@@ -6545,6 +6629,7 @@ class HttpCli(object):
6545
6629
  "acct": self.uname,
6546
6630
  "perms": perms,
6547
6631
  }
6632
+ # also see `js_htm` in authsrv.py
6548
6633
  j2a = {
6549
6634
  "cgv1": vn.js_htm,
6550
6635
  "cgv": cgv,
@@ -259,7 +259,7 @@ class SMB(object):
259
259
  0,
260
260
  "1.7.6.2",
261
261
  time.time(),
262
- "",
262
+ None,
263
263
  )
264
264
  t = hr.get("rejectmsg") or ""
265
265
  if t or not hr:
@@ -285,6 +285,9 @@ class SvcHub(object):
285
285
  ch = "abcdefghijklmnopqrstuvwx"[int(args.theme / 2)]
286
286
  args.theme = "{0}{1} {0} {1}".format(ch, bri)
287
287
 
288
+ if args.no_stack:
289
+ args.stack_who = "no"
290
+
288
291
  if args.nid:
289
292
  args.du_who = "no"
290
293
  args.du_iwho = n_du_who(args.du_who)