copyparty 1.12.1__tar.gz → 1.13.0__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 (119) hide show
  1. {copyparty-1.12.1 → copyparty-1.13.0}/PKG-INFO +29 -4
  2. {copyparty-1.12.1 → copyparty-1.13.0}/README.md +28 -3
  3. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/__main__.py +7 -2
  4. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/__version__.py +3 -3
  5. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/authsrv.py +8 -7
  6. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/cert.py +17 -6
  7. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/cfg.py +3 -0
  8. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/httpcli.py +188 -10
  9. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/httpconn.py +1 -0
  10. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/httpsrv.py +2 -0
  11. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/svchub.py +13 -0
  12. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/th_srv.py +2 -1
  13. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/up2k.py +124 -60
  14. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/util.py +101 -29
  15. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/web/a/u2c.py +8 -4
  16. copyparty-1.13.0/copyparty/web/browser.css.gz +0 -0
  17. copyparty-1.13.0/copyparty/web/browser.js.gz +0 -0
  18. copyparty-1.13.0/copyparty/web/splash.css.gz +0 -0
  19. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/web/splash.html +1 -1
  20. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/web/up2k.js.gz +0 -0
  21. copyparty-1.13.0/copyparty/web/util.js.gz +0 -0
  22. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty.egg-info/PKG-INFO +29 -4
  23. copyparty-1.12.1/copyparty/web/browser.css.gz +0 -0
  24. copyparty-1.12.1/copyparty/web/browser.js.gz +0 -0
  25. copyparty-1.12.1/copyparty/web/splash.css.gz +0 -0
  26. copyparty-1.12.1/copyparty/web/util.js.gz +0 -0
  27. {copyparty-1.12.1 → copyparty-1.13.0}/LICENSE +0 -0
  28. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/__init__.py +0 -0
  29. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/bos/__init__.py +0 -0
  30. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/bos/bos.py +0 -0
  31. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/bos/path.py +0 -0
  32. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/broker_mp.py +0 -0
  33. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/broker_mpw.py +0 -0
  34. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/broker_thr.py +0 -0
  35. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/broker_util.py +0 -0
  36. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/dxml.py +0 -0
  37. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/fsutil.py +0 -0
  38. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/ftpd.py +0 -0
  39. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/ico.py +0 -0
  40. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/mdns.py +0 -0
  41. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/metrics.py +0 -0
  42. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/mtag.py +0 -0
  43. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/multicast.py +0 -0
  44. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/pwhash.py +0 -0
  45. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/res/COPYING.txt +0 -0
  46. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/res/__init__.py +0 -0
  47. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/res/insecure.pem +0 -0
  48. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/smbd.py +0 -0
  49. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/ssdp.py +0 -0
  50. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/star.py +0 -0
  51. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/stolen/__init__.py +0 -0
  52. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/stolen/dnslib/__init__.py +0 -0
  53. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/stolen/dnslib/bimap.py +0 -0
  54. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/stolen/dnslib/bit.py +0 -0
  55. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/stolen/dnslib/buffer.py +0 -0
  56. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/stolen/dnslib/dns.py +0 -0
  57. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/stolen/dnslib/label.py +0 -0
  58. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/stolen/dnslib/lex.py +0 -0
  59. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/stolen/dnslib/ranges.py +0 -0
  60. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/stolen/ifaddr/__init__.py +0 -0
  61. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/stolen/ifaddr/_posix.py +0 -0
  62. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/stolen/ifaddr/_shared.py +0 -0
  63. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/stolen/ifaddr/_win32.py +0 -0
  64. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/stolen/qrcodegen.py +0 -0
  65. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/stolen/surrogateescape.py +0 -0
  66. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/sutil.py +0 -0
  67. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/szip.py +0 -0
  68. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/tcpsrv.py +0 -0
  69. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/tftpd.py +0 -0
  70. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/th_cli.py +0 -0
  71. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/u2idx.py +0 -0
  72. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/web/a/__init__.py +0 -0
  73. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/web/a/partyfuse.py +0 -0
  74. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/web/a/webdav-cfg.bat +0 -0
  75. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/web/baguettebox.js.gz +0 -0
  76. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/web/browser.html +0 -0
  77. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/web/browser2.html +0 -0
  78. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/web/cf.html +0 -0
  79. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/web/dbg-audio.js.gz +0 -0
  80. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/web/dd/2.png +0 -0
  81. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/web/dd/3.png +0 -0
  82. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/web/dd/4.png +0 -0
  83. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/web/dd/5.png +0 -0
  84. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/web/dd/__init__.py +0 -0
  85. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/web/deps/__init__.py +0 -0
  86. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/web/deps/busy.mp3.gz +0 -0
  87. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/web/deps/easymde.css.gz +0 -0
  88. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/web/deps/easymde.js.gz +0 -0
  89. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/web/deps/marked.js.gz +0 -0
  90. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/web/deps/mini-fa.css.gz +0 -0
  91. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/web/deps/mini-fa.woff +0 -0
  92. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/web/deps/prism.css.gz +0 -0
  93. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/web/deps/prism.js.gz +0 -0
  94. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/web/deps/prismd.css.gz +0 -0
  95. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/web/deps/scp.woff2 +0 -0
  96. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/web/deps/sha512.ac.js.gz +0 -0
  97. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/web/deps/sha512.hw.js.gz +0 -0
  98. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/web/md.css.gz +0 -0
  99. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/web/md.html +0 -0
  100. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/web/md.js.gz +0 -0
  101. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/web/md2.css.gz +0 -0
  102. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/web/md2.js.gz +0 -0
  103. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/web/mde.css.gz +0 -0
  104. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/web/mde.html +0 -0
  105. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/web/mde.js.gz +0 -0
  106. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/web/msg.css.gz +0 -0
  107. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/web/msg.html +0 -0
  108. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/web/splash.js.gz +0 -0
  109. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/web/svcs.html +0 -0
  110. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/web/svcs.js.gz +0 -0
  111. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/web/ui.css.gz +0 -0
  112. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty/web/w.hash.js.gz +0 -0
  113. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty.egg-info/SOURCES.txt +0 -0
  114. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty.egg-info/dependency_links.txt +0 -0
  115. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty.egg-info/entry_points.txt +0 -0
  116. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty.egg-info/requires.txt +0 -0
  117. {copyparty-1.12.1 → copyparty-1.13.0}/copyparty.egg-info/top_level.txt +0 -0
  118. {copyparty-1.12.1 → copyparty-1.13.0}/pyproject.toml +0 -0
  119. {copyparty-1.12.1 → copyparty-1.13.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: copyparty
3
- Version: 1.12.1
3
+ Version: 1.13.0
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
@@ -64,13 +64,16 @@ turn almost any device into a file server with resumable uploads/downloads using
64
64
 
65
65
  📷 **screenshots:** [browser](#the-browser) // [upload](#uploading) // [unpost](#unpost) // [thumbnails](#thumbnails) // [search](#searching) // [fsearch](#file-search) // [zip-DL](#zip-downloads) // [md-viewer](#markdown-viewer)
66
66
 
67
+ 🎬 **videos:** [upload](https://a.ocv.me/pub/demo/pics-vids/up2k.webm) // [cli-upload](https://a.ocv.me/pub/demo/pics-vids/u2cli.webm) // [race-the-beam](https://a.ocv.me/pub/g/nerd-stuff/cpp/2024-0418-race-the-beam.webm)
68
+
67
69
 
68
70
  ## readme toc
69
71
 
70
72
  * top
71
73
  * [quickstart](#quickstart) - just run **[copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py)** -- that's it! 🎉
74
+ * [at home](#at-home) - make it accessible over the internet
72
75
  * [on servers](#on-servers) - you may also want these, especially on servers
73
- * [features](#features)
76
+ * [features](#features) - also see [comparison to similar software](./docs/versus.md)
74
77
  * [testimonials](#testimonials) - small collection of user feedback
75
78
  * [motivations](#motivations) - project goals / philosophy
76
79
  * [notes](#notes) - general notes
@@ -91,6 +94,7 @@ turn almost any device into a file server with resumable uploads/downloads using
91
94
  * [file-search](#file-search) - dropping files into the browser also lets you see if they exist on the server
92
95
  * [unpost](#unpost) - undo/delete accidental uploads
93
96
  * [self-destruct](#self-destruct) - uploads can be given a lifetime
97
+ * [race the beam](#race-the-beam) - download files while they're still uploading ([demo video](http://a.ocv.me/pub/g/nerd-stuff/cpp/2024-0418-race-the-beam.webm))
94
98
  * [file manager](#file-manager) - cut/paste, rename, and delete files/folders (if you have permission)
95
99
  * [batch rename](#batch-rename) - select some files and press `F2` to bring up the rename UI
96
100
  * [media player](#media-player) - plays almost every audio format there is
@@ -180,7 +184,7 @@ enable thumbnails (images/audio/video), media indexing, and audio transcoding by
180
184
 
181
185
  * **Alpine:** `apk add py3-pillow ffmpeg`
182
186
  * **Debian:** `apt install --no-install-recommends python3-pil ffmpeg`
183
- * **Fedora:** rpmfusion + `dnf install python3-pillow ffmpeg`
187
+ * **Fedora:** rpmfusion + `dnf install python3-pillow ffmpeg --allowerasing`
184
188
  * **FreeBSD:** `pkg install py39-sqlite3 py39-pillow ffmpeg`
185
189
  * **MacOS:** `port install py-Pillow ffmpeg`
186
190
  * **MacOS** (alternative): `brew install pillow ffmpeg`
@@ -201,6 +205,17 @@ some recommended options:
201
205
  * see [accounts and volumes](#accounts-and-volumes) (or `--help-accounts`) for the syntax and other permissions
202
206
 
203
207
 
208
+ ### at home
209
+
210
+ make it accessible over the internet by starting a [cloudflare quicktunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/do-more-with-tunnels/trycloudflare/) like so:
211
+
212
+ first download [cloudflared](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/) and then start the tunnel with `cloudflared tunnel --url http://127.0.0.1:3923`
213
+
214
+ as the tunnel starts, it will show a URL which you can share to let anyone browse your stash or upload files to you
215
+
216
+ since people will be connecting through cloudflare, run copyparty with `--xff-hdr cf-connecting-ip` to detect client IPs correctly
217
+
218
+
204
219
  ### on servers
205
220
 
206
221
  you may also want these, especially on servers:
@@ -224,6 +239,8 @@ firewall-cmd --reload
224
239
 
225
240
  ## features
226
241
 
242
+ also see [comparison to similar software](./docs/versus.md)
243
+
227
244
  * backend stuff
228
245
  * ☑ IPv6
229
246
  * ☑ [multiprocessing](#performance) (actual multithreading)
@@ -246,6 +263,7 @@ firewall-cmd --reload
246
263
  * ☑ write-only folders
247
264
  * ☑ [unpost](#unpost): undo/delete accidental uploads
248
265
  * ☑ [self-destruct](#self-destruct) (specified server-side or client-side)
266
+ * ☑ [race the beam](#race-the-beam) (almost like peer-to-peer)
249
267
  * ☑ symlink/discard duplicates (content-matching)
250
268
  * download
251
269
  * ☑ single files in browser
@@ -671,7 +689,7 @@ up2k has several advantages:
671
689
  > it is perfectly safe to restart / upgrade copyparty while someone is uploading to it!
672
690
  > all known up2k clients will resume just fine 💪
673
691
 
674
- see [up2k](#up2k) for details on how it works, or watch a [demo video](https://a.ocv.me/pub/demo/pics-vids/#gf-0f6f5c0d)
692
+ see [up2k](./docs/devnotes.md#up2k) for details on how it works, or watch a [demo video](https://a.ocv.me/pub/demo/pics-vids/#gf-0f6f5c0d)
675
693
 
676
694
  ![copyparty-upload-fs8](https://user-images.githubusercontent.com/241032/129635371-48fc54ca-fa91-48e3-9b1d-ba413e4b68cb.png)
677
695
 
@@ -737,6 +755,13 @@ clients can specify a shorter expiration time using the [up2k ui](#uploading) --
737
755
  specifying a custom expiration time client-side will affect the timespan in which unposts are permitted, so keep an eye on the estimates in the up2k ui
738
756
 
739
757
 
758
+ ### race the beam
759
+
760
+ download files while they're still uploading ([demo video](http://a.ocv.me/pub/g/nerd-stuff/cpp/2024-0418-race-the-beam.webm)) -- it's almost like peer-to-peer
761
+
762
+ requires the file to be uploaded using up2k (which is the default drag-and-drop uploader), alternatively the command-line program
763
+
764
+
740
765
  ## file manager
741
766
 
742
767
  cut/paste, rename, and delete files/folders (if you have permission)
@@ -10,13 +10,16 @@ turn almost any device into a file server with resumable uploads/downloads using
10
10
 
11
11
  📷 **screenshots:** [browser](#the-browser) // [upload](#uploading) // [unpost](#unpost) // [thumbnails](#thumbnails) // [search](#searching) // [fsearch](#file-search) // [zip-DL](#zip-downloads) // [md-viewer](#markdown-viewer)
12
12
 
13
+ 🎬 **videos:** [upload](https://a.ocv.me/pub/demo/pics-vids/up2k.webm) // [cli-upload](https://a.ocv.me/pub/demo/pics-vids/u2cli.webm) // [race-the-beam](https://a.ocv.me/pub/g/nerd-stuff/cpp/2024-0418-race-the-beam.webm)
14
+
13
15
 
14
16
  ## readme toc
15
17
 
16
18
  * top
17
19
  * [quickstart](#quickstart) - just run **[copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py)** -- that's it! 🎉
20
+ * [at home](#at-home) - make it accessible over the internet
18
21
  * [on servers](#on-servers) - you may also want these, especially on servers
19
- * [features](#features)
22
+ * [features](#features) - also see [comparison to similar software](./docs/versus.md)
20
23
  * [testimonials](#testimonials) - small collection of user feedback
21
24
  * [motivations](#motivations) - project goals / philosophy
22
25
  * [notes](#notes) - general notes
@@ -37,6 +40,7 @@ turn almost any device into a file server with resumable uploads/downloads using
37
40
  * [file-search](#file-search) - dropping files into the browser also lets you see if they exist on the server
38
41
  * [unpost](#unpost) - undo/delete accidental uploads
39
42
  * [self-destruct](#self-destruct) - uploads can be given a lifetime
43
+ * [race the beam](#race-the-beam) - download files while they're still uploading ([demo video](http://a.ocv.me/pub/g/nerd-stuff/cpp/2024-0418-race-the-beam.webm))
40
44
  * [file manager](#file-manager) - cut/paste, rename, and delete files/folders (if you have permission)
41
45
  * [batch rename](#batch-rename) - select some files and press `F2` to bring up the rename UI
42
46
  * [media player](#media-player) - plays almost every audio format there is
@@ -126,7 +130,7 @@ enable thumbnails (images/audio/video), media indexing, and audio transcoding by
126
130
 
127
131
  * **Alpine:** `apk add py3-pillow ffmpeg`
128
132
  * **Debian:** `apt install --no-install-recommends python3-pil ffmpeg`
129
- * **Fedora:** rpmfusion + `dnf install python3-pillow ffmpeg`
133
+ * **Fedora:** rpmfusion + `dnf install python3-pillow ffmpeg --allowerasing`
130
134
  * **FreeBSD:** `pkg install py39-sqlite3 py39-pillow ffmpeg`
131
135
  * **MacOS:** `port install py-Pillow ffmpeg`
132
136
  * **MacOS** (alternative): `brew install pillow ffmpeg`
@@ -147,6 +151,17 @@ some recommended options:
147
151
  * see [accounts and volumes](#accounts-and-volumes) (or `--help-accounts`) for the syntax and other permissions
148
152
 
149
153
 
154
+ ### at home
155
+
156
+ make it accessible over the internet by starting a [cloudflare quicktunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/do-more-with-tunnels/trycloudflare/) like so:
157
+
158
+ first download [cloudflared](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/) and then start the tunnel with `cloudflared tunnel --url http://127.0.0.1:3923`
159
+
160
+ as the tunnel starts, it will show a URL which you can share to let anyone browse your stash or upload files to you
161
+
162
+ since people will be connecting through cloudflare, run copyparty with `--xff-hdr cf-connecting-ip` to detect client IPs correctly
163
+
164
+
150
165
  ### on servers
151
166
 
152
167
  you may also want these, especially on servers:
@@ -170,6 +185,8 @@ firewall-cmd --reload
170
185
 
171
186
  ## features
172
187
 
188
+ also see [comparison to similar software](./docs/versus.md)
189
+
173
190
  * backend stuff
174
191
  * ☑ IPv6
175
192
  * ☑ [multiprocessing](#performance) (actual multithreading)
@@ -192,6 +209,7 @@ firewall-cmd --reload
192
209
  * ☑ write-only folders
193
210
  * ☑ [unpost](#unpost): undo/delete accidental uploads
194
211
  * ☑ [self-destruct](#self-destruct) (specified server-side or client-side)
212
+ * ☑ [race the beam](#race-the-beam) (almost like peer-to-peer)
195
213
  * ☑ symlink/discard duplicates (content-matching)
196
214
  * download
197
215
  * ☑ single files in browser
@@ -617,7 +635,7 @@ up2k has several advantages:
617
635
  > it is perfectly safe to restart / upgrade copyparty while someone is uploading to it!
618
636
  > all known up2k clients will resume just fine 💪
619
637
 
620
- see [up2k](#up2k) for details on how it works, or watch a [demo video](https://a.ocv.me/pub/demo/pics-vids/#gf-0f6f5c0d)
638
+ see [up2k](./docs/devnotes.md#up2k) for details on how it works, or watch a [demo video](https://a.ocv.me/pub/demo/pics-vids/#gf-0f6f5c0d)
621
639
 
622
640
  ![copyparty-upload-fs8](https://user-images.githubusercontent.com/241032/129635371-48fc54ca-fa91-48e3-9b1d-ba413e4b68cb.png)
623
641
 
@@ -683,6 +701,13 @@ clients can specify a shorter expiration time using the [up2k ui](#uploading) --
683
701
  specifying a custom expiration time client-side will affect the timespan in which unposts are permitted, so keep an eye on the estimates in the up2k ui
684
702
 
685
703
 
704
+ ### race the beam
705
+
706
+ download files while they're still uploading ([demo video](http://a.ocv.me/pub/g/nerd-stuff/cpp/2024-0418-race-the-beam.webm)) -- it's almost like peer-to-peer
707
+
708
+ requires the file to be uploaded using up2k (which is the default drag-and-drop uploader), alternatively the command-line program
709
+
710
+
686
711
  ## file manager
687
712
 
688
713
  cut/paste, rename, and delete files/folders (if you have permission)
@@ -850,8 +850,9 @@ def add_qr(ap, tty):
850
850
 
851
851
  def add_fs(ap):
852
852
  ap2 = ap.add_argument_group("filesystem options")
853
- rm_re_def = "5/0.1" if ANYWIN else "0/0"
853
+ rm_re_def = "15/0.1" if ANYWIN else "0/0"
854
854
  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)")
855
+ 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)")
855
856
  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)")
856
857
 
857
858
 
@@ -943,6 +944,8 @@ def add_auth(ap):
943
944
  ap2.add_argument("--idp-h-grp", metavar="HN", type=u, default="", help="assume the request-header \033[33mHN\033[0m contains the groupname of the requesting user; can be referenced in config files for group-based access control")
944
945
  ap2.add_argument("--idp-h-key", metavar="HN", type=u, default="", help="optional but recommended safeguard; your reverse-proxy will insert a secret header named \033[33mHN\033[0m into all requests, and the other IdP headers will be ignored if this header is not present")
945
946
  ap2.add_argument("--idp-gsep", metavar="RE", type=u, default="|:;+,", help="if there are multiple groups in \033[33m--idp-h-grp\033[0m, they are separated by one of the characters in \033[33mRE\033[0m")
947
+ 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")
948
+ 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")
946
949
 
947
950
 
948
951
  def add_zeroconf(ap):
@@ -1082,6 +1085,8 @@ def add_optouts(ap):
1082
1085
  ap2.add_argument("--no-zip", action="store_true", help="disable download as zip/tar")
1083
1086
  ap2.add_argument("--no-tarcmp", action="store_true", help="disable download as compressed tar (?tar=gz, ?tar=bz2, ?tar=xz, ?tar=gz:9, ...)")
1084
1087
  ap2.add_argument("--no-lifetime", action="store_true", help="do not allow clients (or server config) to schedule an upload to be deleted after a given time")
1088
+ ap2.add_argument("--no-pipe", action="store_true", help="disable race-the-beam (lockstep download of files which are currently being uploaded) (volflag=nopipe)")
1089
+ ap2.add_argument("--no-db-ip", action="store_true", help="do not write uploader IPs into the database")
1085
1090
 
1086
1091
 
1087
1092
  def add_safety(ap):
@@ -1207,7 +1212,7 @@ def add_db_general(ap, hcores):
1207
1212
  ap2.add_argument("--no-hash", metavar="PTN", type=u, help="regex: disable hashing of matching absolute-filesystem-paths during e2ds folder scans (volflag=nohash)")
1208
1213
  ap2.add_argument("--no-idx", metavar="PTN", type=u, default=noidx, help="regex: disable indexing of matching absolute-filesystem-paths during e2ds folder scans (volflag=noidx)")
1209
1214
  ap2.add_argument("--no-dhash", action="store_true", help="disable rescan acceleration; do full database integrity check -- makes the db ~5%% smaller and bootup/rescans 3~10x slower")
1210
- ap2.add_argument("--re-dhash", action="store_true", help="rebuild the cache if it gets out of sync (for example crash on startup during metadata scanning)")
1215
+ ap2.add_argument("--re-dhash", action="store_true", help="force a cache rebuild on startup; enable this once if it gets out of sync (should never be necessary)")
1211
1216
  ap2.add_argument("--no-forget", action="store_true", help="never forget indexed files, even when deleted from disk -- makes it impossible to ever upload the same file twice -- only useful for offloading uploads to a cloud service or something (volflag=noforget)")
1212
1217
  ap2.add_argument("--dbd", metavar="PROFILE", default="wal", help="database durability profile; sets the tradeoff between robustness and speed, see \033[33m--help-dbd\033[0m (volflag=dbd)")
1213
1218
  ap2.add_argument("--xlink", action="store_true", help="on upload: check all volumes for dupes, not just the target volume (volflag=xlink)")
@@ -1,8 +1,8 @@
1
1
  # coding: utf-8
2
2
 
3
- VERSION = (1, 12, 1)
4
- CODENAME = "locksmith"
5
- BUILD_DT = (2024, 4, 9)
3
+ VERSION = (1, 13, 0)
4
+ CODENAME = "race the beam"
5
+ BUILD_DT = (2024, 4, 20)
6
6
 
7
7
  S_VERSION = ".".join(map(str, VERSION))
8
8
  S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)
@@ -1757,13 +1757,14 @@ class AuthSrv(object):
1757
1757
  if k in vol.flags:
1758
1758
  vol.flags[k] = float(vol.flags[k])
1759
1759
 
1760
- try:
1761
- zs1, zs2 = vol.flags["rm_retry"].split("/")
1762
- vol.flags["rm_re_t"] = float(zs1)
1763
- vol.flags["rm_re_r"] = float(zs2)
1764
- except:
1765
- t = 'volume "/%s" has invalid rm_retry [%s]'
1766
- raise Exception(t % (vol.vpath, vol.flags.get("rm_retry")))
1760
+ for k in ("mv_re", "rm_re"):
1761
+ try:
1762
+ zs1, zs2 = vol.flags[k + "try"].split("/")
1763
+ vol.flags[k + "_t"] = float(zs1)
1764
+ vol.flags[k + "_r"] = float(zs2)
1765
+ except:
1766
+ t = 'volume "/%s" has invalid %stry [%s]'
1767
+ raise Exception(t % (vol.vpath, k, vol.flags.get(k + "try")))
1767
1768
 
1768
1769
  for k1, k2 in IMPLICATIONS:
1769
1770
  if k1 in vol.flags:
@@ -6,10 +6,17 @@ import os
6
6
  import shutil
7
7
  import time
8
8
 
9
- from .util import Netdev, runcmd
9
+ from .__init__ import ANYWIN
10
+ from .util import Netdev, runcmd, wrename, wunlink
10
11
 
11
12
  HAVE_CFSSL = True
12
13
 
14
+ if ANYWIN:
15
+ VF = {"mv_re_t": 5, "rm_re_t": 5, "mv_re_r": 0.1, "rm_re_r": 0.1}
16
+ else:
17
+ VF = {"mv_re_t": 0, "rm_re_t": 0}
18
+
19
+
13
20
  def ensure_cert(log , args) :
14
21
  """
15
22
  the default cert (and the entire TLS support) is only here to enable the
@@ -101,8 +108,12 @@ def _gen_ca(log , args):
101
108
  raise Exception("failed to translate ca-cert: {}, {}".format(rc, se), 3)
102
109
 
103
110
  bname = os.path.join(args.crt_dir, "ca")
104
- os.rename(bname + "-key.pem", bname + ".key")
105
- os.unlink(bname + ".csr")
111
+ try:
112
+ wunlink(log, bname + ".key", VF)
113
+ except:
114
+ pass
115
+ wrename(log, bname + "-key.pem", bname + ".key", VF)
116
+ wunlink(log, bname + ".csr", VF)
106
117
 
107
118
  log("cert", "new ca OK", 2)
108
119
 
@@ -181,11 +192,11 @@ def _gen_srv(log , args, netdevs ):
181
192
 
182
193
  bname = os.path.join(args.crt_dir, "srv")
183
194
  try:
184
- os.unlink(bname + ".key")
195
+ wunlink(log, bname + ".key", VF)
185
196
  except:
186
197
  pass
187
- os.rename(bname + "-key.pem", bname + ".key")
188
- os.unlink(bname + ".csr")
198
+ wrename(log, bname + "-key.pem", bname + ".key", VF)
199
+ wunlink(log, bname + ".csr", VF)
189
200
 
190
201
  with open(os.path.join(args.crt_dir, "ca.pem"), "rb") as f:
191
202
  ca = f.read()
@@ -16,6 +16,7 @@ def vf_bmap() :
16
16
  "no_dedup": "copydupes",
17
17
  "no_dupe": "nodupe",
18
18
  "no_forget": "noforget",
19
+ "no_pipe": "nopipe",
19
20
  "no_robots": "norobots",
20
21
  "no_thumb": "dthumb",
21
22
  "no_vthumb": "dvthumb",
@@ -63,6 +64,7 @@ def vf_vmap() :
63
64
  "lg_sbf",
64
65
  "md_sbf",
65
66
  "nrand",
67
+ "mv_retry",
66
68
  "rm_retry",
67
69
  "sort",
68
70
  "unlist",
@@ -214,6 +216,7 @@ flagcats = {
214
216
  "dots": "allow all users with read-access to\nenable the option to show dotfiles in listings",
215
217
  "fk=8": 'generates per-file accesskeys,\nwhich are then required at the "g" permission;\nkeys are invalidated if filesize or inode changes',
216
218
  "fka=8": 'generates slightly weaker per-file accesskeys,\nwhich are then required at the "g" permission;\nnot affected by filesize or inode numbers',
219
+ "mv_retry": "ms-windows: timeout for renaming busy files",
217
220
  "rm_retry": "ms-windows: timeout for deleting busy files",
218
221
  "davauth": "ask webdav clients to login for all folders",
219
222
  "davrt": "show lastmod time of symlink destination, not the link itself\n(note: this option is always enabled for recursive listings)",
@@ -36,6 +36,7 @@ from .bos import bos
36
36
  from .star import StreamTar
37
37
  from .sutil import StreamArc, gfilter
38
38
  from .szip import StreamZip
39
+ from .up2k import up2k_chunksize
39
40
  from .util import unquote # type: ignore
40
41
  from .util import (
41
42
  APPLESAN_RE,
@@ -89,6 +90,7 @@ from .util import (
89
90
  vjoin,
90
91
  vol_san,
91
92
  vsplit,
93
+ wrename,
92
94
  wunlink,
93
95
  yieldfile,
94
96
  )
@@ -122,6 +124,7 @@ class HttpCli(object):
122
124
  self.ico = conn.ico # mypy404
123
125
  self.thumbcli = conn.thumbcli # mypy404
124
126
  self.u2fh = conn.u2fh # mypy404
127
+ self.pipes = conn.pipes # mypy404
125
128
  self.log_func = conn.log_func # mypy404
126
129
  self.log_src = conn.log_src # mypy404
127
130
  self.gen_fk = self._gen_fk if self.args.log_fk else gen_filekey
@@ -439,7 +442,11 @@ class HttpCli(object):
439
442
 
440
443
  zso = self.headers.get("authorization")
441
444
  bauth = ""
442
- if zso:
445
+ if (
446
+ zso
447
+ and not self.args.no_bauth
448
+ and (not cookie_pw or not self.args.bauth_last)
449
+ ):
443
450
  try:
444
451
  zb = zso.split(" ")[1].encode("ascii")
445
452
  zs = base64.b64decode(zb).decode("utf-8")
@@ -1796,7 +1803,7 @@ class HttpCli(object):
1796
1803
  f, fn = zfw["orz"]
1797
1804
 
1798
1805
  path2 = os.path.join(fdir, fn2)
1799
- atomic_move(path, path2)
1806
+ atomic_move(self.log, path, path2, vfs.flags)
1800
1807
  fn = fn2
1801
1808
  path = path2
1802
1809
 
@@ -1877,7 +1884,9 @@ class HttpCli(object):
1877
1884
  self.reply(t.encode("utf-8"), 201, headers=h)
1878
1885
  return True
1879
1886
 
1880
- def bakflip(self, f , ofs , sz , sha ) :
1887
+ def bakflip(
1888
+ self, f , ofs , sz , sha , flags
1889
+ ) :
1881
1890
  if not self.args.bak_flips or self.args.nw:
1882
1891
  return
1883
1892
 
@@ -1905,7 +1914,7 @@ class HttpCli(object):
1905
1914
 
1906
1915
  if nrem:
1907
1916
  self.log("bakflip truncated; {} remains".format(nrem), 1)
1908
- atomic_move(fp, fp + ".trunc")
1917
+ atomic_move(self.log, fp, fp + ".trunc", flags)
1909
1918
  else:
1910
1919
  self.log("bakflip ok", 2)
1911
1920
 
@@ -2171,7 +2180,7 @@ class HttpCli(object):
2171
2180
 
2172
2181
  if sha_b64 != chash:
2173
2182
  try:
2174
- self.bakflip(f, cstart[0], post_sz, sha_b64)
2183
+ self.bakflip(f, cstart[0], post_sz, sha_b64, vfs.flags)
2175
2184
  except:
2176
2185
  self.log("bakflip failed: " + min_ex())
2177
2186
 
@@ -2523,7 +2532,7 @@ class HttpCli(object):
2523
2532
  raise
2524
2533
 
2525
2534
  if not nullwrite:
2526
- atomic_move(tabspath, abspath)
2535
+ atomic_move(self.log, tabspath, abspath, vfs.flags)
2527
2536
 
2528
2537
  tabspath = ""
2529
2538
 
@@ -2763,7 +2772,7 @@ class HttpCli(object):
2763
2772
  hidedir(dp)
2764
2773
  except:
2765
2774
  pass
2766
- bos.rename(fp, os.path.join(mdir, ".hist", mfile2))
2775
+ wrename(self.log, fp, os.path.join(mdir, ".hist", mfile2), vfs.flags)
2767
2776
 
2768
2777
  assert self.parser.gen
2769
2778
  p_field, _, p_data = next(self.parser.gen)
@@ -2918,17 +2927,42 @@ class HttpCli(object):
2918
2927
 
2919
2928
  return txt
2920
2929
 
2921
- def tx_file(self, req_path ) :
2930
+ def tx_file(self, req_path , ptop = None) :
2922
2931
  status = 200
2923
2932
  logmsg = "{:4} {} ".format("", self.req)
2924
2933
  logtail = ""
2925
2934
 
2935
+ if ptop is not None:
2936
+ try:
2937
+ dp, fn = os.path.split(req_path)
2938
+ tnam = fn + ".PARTIAL"
2939
+ if self.args.dotpart:
2940
+ tnam = "." + tnam
2941
+ ap_data = os.path.join(dp, tnam)
2942
+ st_data = bos.stat(ap_data)
2943
+ if not st_data.st_size:
2944
+ raise Exception("partial is empty")
2945
+ x = self.conn.hsrv.broker.ask("up2k.find_job_by_ap", ptop, req_path)
2946
+ job = json.loads(x.get())
2947
+ if not job:
2948
+ raise Exception("not found in registry")
2949
+ self.pipes.set(req_path, job)
2950
+ except Exception as ex:
2951
+ self.log("will not pipe [%s]; %s" % (ap_data, ex), 6)
2952
+ ptop = None
2953
+
2926
2954
  #
2927
2955
  # if request is for foo.js, check if we have foo.js.gz
2928
2956
 
2929
2957
  file_ts = 0.0
2930
2958
  editions = {}
2931
2959
  for ext in ("", ".gz"):
2960
+ if ptop is not None:
2961
+ sz = job["size"]
2962
+ file_ts = job["lmod"]
2963
+ editions["plain"] = (ap_data, sz)
2964
+ break
2965
+
2932
2966
  try:
2933
2967
  fs_path = req_path + ext
2934
2968
  st = bos.stat(fs_path)
@@ -3085,6 +3119,11 @@ class HttpCli(object):
3085
3119
  self.send_headers(length=upper - lower, status=status, mime=mime)
3086
3120
  return True
3087
3121
 
3122
+ if ptop is not None:
3123
+ return self.tx_pipe(
3124
+ ptop, req_path, ap_data, job, lower, upper, status, mime, logmsg
3125
+ )
3126
+
3088
3127
  ret = True
3089
3128
  with open_func(*open_args) as f:
3090
3129
  self.send_headers(length=upper - lower, status=status, mime=mime)
@@ -3104,6 +3143,143 @@ class HttpCli(object):
3104
3143
 
3105
3144
  return ret
3106
3145
 
3146
+ def tx_pipe(
3147
+ self,
3148
+ ptop ,
3149
+ req_path ,
3150
+ ap_data ,
3151
+ job ,
3152
+ lower ,
3153
+ upper ,
3154
+ status ,
3155
+ mime ,
3156
+ logmsg ,
3157
+ ) :
3158
+ M = 1048576
3159
+ self.send_headers(length=upper - lower, status=status, mime=mime)
3160
+ wr_slp = self.args.s_wr_slp
3161
+ wr_sz = self.args.s_wr_sz
3162
+ file_size = job["size"]
3163
+ chunk_size = up2k_chunksize(file_size)
3164
+ num_need = -1
3165
+ data_end = 0
3166
+ remains = upper - lower
3167
+ broken = False
3168
+ spins = 0
3169
+ tier = 0
3170
+ tiers = ["uncapped", "reduced speed", "one byte per sec"]
3171
+
3172
+ while lower < upper and not broken:
3173
+ with self.u2mutex:
3174
+ job = self.pipes.get(req_path)
3175
+ if not job:
3176
+ x = self.conn.hsrv.broker.ask("up2k.find_job_by_ap", ptop, req_path)
3177
+ job = json.loads(x.get())
3178
+ if job:
3179
+ self.pipes.set(req_path, job)
3180
+
3181
+ if not job:
3182
+ t = "pipe: OK, upload has finished; yeeting remainder"
3183
+ self.log(t, 2)
3184
+ data_end = file_size
3185
+ break
3186
+
3187
+ if num_need != len(job["need"]):
3188
+ num_need = len(job["need"])
3189
+ data_end = 0
3190
+ for cid in job["hash"]:
3191
+ if cid in job["need"]:
3192
+ break
3193
+ data_end += chunk_size
3194
+ t = "pipe: can stream %.2f MiB; requested range is %.2f to %.2f"
3195
+ self.log(t % (data_end / M, lower / M, upper / M), 6)
3196
+ with self.u2mutex:
3197
+ if data_end > self.u2fh.aps.get(ap_data, data_end):
3198
+ try:
3199
+ fhs = self.u2fh.cache[ap_data].all_fhs
3200
+ for fh in fhs:
3201
+ fh.flush()
3202
+ self.u2fh.aps[ap_data] = data_end
3203
+ self.log("pipe: flushed %d up2k-FDs" % (len(fhs),))
3204
+ except Exception as ex:
3205
+ self.log("pipe: u2fh flush failed: %r" % (ex,))
3206
+
3207
+ if lower >= data_end:
3208
+ if data_end:
3209
+ t = "pipe: uploader is too slow; aborting download at %.2f MiB"
3210
+ self.log(t % (data_end / M))
3211
+ raise Pebkac(416, "uploader is too slow")
3212
+
3213
+ raise Pebkac(416, "no data available yet; please retry in a bit")
3214
+
3215
+ slack = data_end - lower
3216
+ if slack >= 8 * M:
3217
+ ntier = 0
3218
+ winsz = M
3219
+ bufsz = wr_sz
3220
+ slp = wr_slp
3221
+ else:
3222
+ winsz = max(40, int(M * (slack / (12 * M))))
3223
+ base_rate = M if not wr_slp else wr_sz / wr_slp
3224
+ if winsz > base_rate:
3225
+ ntier = 0
3226
+ bufsz = wr_sz
3227
+ slp = wr_slp
3228
+ elif winsz > 300:
3229
+ ntier = 1
3230
+ bufsz = winsz // 5
3231
+ slp = 0.2
3232
+ else:
3233
+ ntier = 2
3234
+ bufsz = winsz = slp = 1
3235
+
3236
+ if tier != ntier:
3237
+ tier = ntier
3238
+ self.log("moved to tier %d (%s)" % (tier, tiers[tier]))
3239
+
3240
+ try:
3241
+ with open(ap_data, "rb", self.args.iobuf) as f:
3242
+ f.seek(lower)
3243
+ page = f.read(min(winsz, data_end - lower, upper - lower))
3244
+ if not page:
3245
+ raise Exception("got 0 bytes (EOF?)")
3246
+ except Exception as ex:
3247
+ self.log("pipe: read failed at %.2f MiB: %s" % (lower / M, ex), 3)
3248
+ with self.u2mutex:
3249
+ self.pipes.c.pop(req_path, None)
3250
+ spins += 1
3251
+ if spins > 3:
3252
+ raise Pebkac(500, "file became unreadable")
3253
+ time.sleep(2)
3254
+ continue
3255
+
3256
+ spins = 0
3257
+ pofs = 0
3258
+ while pofs < len(page):
3259
+ if slp:
3260
+ time.sleep(slp)
3261
+
3262
+ try:
3263
+ buf = page[pofs : pofs + bufsz]
3264
+ self.s.sendall(buf)
3265
+ zi = len(buf)
3266
+ remains -= zi
3267
+ lower += zi
3268
+ pofs += zi
3269
+ except:
3270
+ broken = True
3271
+ break
3272
+
3273
+ if lower < upper and not broken:
3274
+ with open(req_path, "rb") as f:
3275
+ remains = sendfile_py(self.log, lower, upper, f, self.s, wr_sz, wr_slp)
3276
+
3277
+ spd = self._spd((upper - lower) - remains)
3278
+ if self.do_log:
3279
+ self.log("{}, {}".format(logmsg, spd))
3280
+
3281
+ return not broken
3282
+
3107
3283
  def tx_zip(
3108
3284
  self,
3109
3285
  fmt ,
@@ -3741,7 +3917,7 @@ class HttpCli(object):
3741
3917
  if not allvols:
3742
3918
  ret = [{"kinshi": 1}]
3743
3919
 
3744
- jtxt = '{"u":%s,"c":%s}' % (uret, json.dumps(ret, indent=0))
3920
+ jtxt = '{"u":%s,"c":%s}' % (uret, json.dumps(ret, separators=(",\n", ": ")))
3745
3921
  zi = len(uret.split('\n"pd":')) - 1
3746
3922
  self.log("%s #%d+%d %.2fsec" % (lm, zi, len(ret), time.time() - t0))
3747
3923
  self.reply(jtxt.encode("utf-8", "replace"), mime="application/json")
@@ -4020,7 +4196,9 @@ class HttpCli(object):
4020
4196
  ):
4021
4197
  return self.tx_md(vn, abspath)
4022
4198
 
4023
- return self.tx_file(abspath)
4199
+ return self.tx_file(
4200
+ abspath, None if st.st_size or "nopipe" in vn.flags else vn.realpath
4201
+ )
4024
4202
 
4025
4203
  elif is_dir and not self.can_read:
4026
4204
  if self._use_dirkey(abspath):
@@ -52,6 +52,7 @@ class HttpConn(object):
52
52
  self.E = self.args.E
53
53
  self.asrv = hsrv.asrv # mypy404
54
54
  self.u2fh = hsrv.u2fh # mypy404
55
+ self.pipes = hsrv.pipes # mypy404
55
56
  self.ipa_nm = hsrv.ipa_nm
56
57
  self.xff_nm = hsrv.xff_nm
57
58
  self.xff_lan = hsrv.xff_lan # type: ignore