copyparty 1.14.1__tar.gz → 1.14.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. {copyparty-1.14.1 → copyparty-1.14.3}/PKG-INFO +16 -5
  2. {copyparty-1.14.1 → copyparty-1.14.3}/README.md +15 -4
  3. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/__main__.py +5 -4
  4. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/__version__.py +2 -2
  5. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/authsrv.py +51 -8
  6. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/httpcli.py +74 -21
  7. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/svchub.py +40 -7
  8. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/tftpd.py +2 -2
  9. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/up2k.py +65 -12
  10. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/a/u2c.py +5 -3
  11. copyparty-1.14.3/copyparty/web/browser.css.gz +0 -0
  12. copyparty-1.14.3/copyparty/web/browser.js.gz +0 -0
  13. copyparty-1.14.3/copyparty/web/shares.css.gz +0 -0
  14. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/shares.html +10 -8
  15. copyparty-1.14.3/copyparty/web/shares.js.gz +0 -0
  16. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/splash.css.gz +0 -0
  17. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/splash.html +33 -15
  18. copyparty-1.14.3/copyparty/web/splash.js.gz +0 -0
  19. copyparty-1.14.3/copyparty/web/ui.css.gz +0 -0
  20. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/util.js.gz +0 -0
  21. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty.egg-info/PKG-INFO +16 -5
  22. copyparty-1.14.1/copyparty/web/browser.css.gz +0 -0
  23. copyparty-1.14.1/copyparty/web/browser.js.gz +0 -0
  24. copyparty-1.14.1/copyparty/web/shares.css.gz +0 -0
  25. copyparty-1.14.1/copyparty/web/shares.js.gz +0 -0
  26. copyparty-1.14.1/copyparty/web/splash.js.gz +0 -0
  27. copyparty-1.14.1/copyparty/web/ui.css.gz +0 -0
  28. {copyparty-1.14.1 → copyparty-1.14.3}/LICENSE +0 -0
  29. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/__init__.py +0 -0
  30. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/bos/__init__.py +0 -0
  31. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/bos/bos.py +0 -0
  32. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/bos/path.py +0 -0
  33. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/broker_mp.py +0 -0
  34. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/broker_mpw.py +0 -0
  35. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/broker_thr.py +0 -0
  36. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/broker_util.py +0 -0
  37. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/cert.py +0 -0
  38. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/cfg.py +0 -0
  39. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/dxml.py +0 -0
  40. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/fsutil.py +0 -0
  41. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/ftpd.py +0 -0
  42. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/httpconn.py +0 -0
  43. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/httpsrv.py +0 -0
  44. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/ico.py +0 -0
  45. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/mdns.py +0 -0
  46. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/metrics.py +0 -0
  47. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/mtag.py +0 -0
  48. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/multicast.py +0 -0
  49. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/pwhash.py +0 -0
  50. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/res/COPYING.txt +0 -0
  51. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/res/__init__.py +0 -0
  52. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/res/insecure.pem +0 -0
  53. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/smbd.py +0 -0
  54. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/ssdp.py +0 -0
  55. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/star.py +0 -0
  56. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/stolen/__init__.py +0 -0
  57. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/stolen/dnslib/__init__.py +0 -0
  58. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/stolen/dnslib/bimap.py +0 -0
  59. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/stolen/dnslib/bit.py +0 -0
  60. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/stolen/dnslib/buffer.py +0 -0
  61. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/stolen/dnslib/dns.py +0 -0
  62. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/stolen/dnslib/label.py +0 -0
  63. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/stolen/dnslib/lex.py +0 -0
  64. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/stolen/dnslib/ranges.py +0 -0
  65. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/stolen/ifaddr/__init__.py +0 -0
  66. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/stolen/ifaddr/_posix.py +0 -0
  67. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/stolen/ifaddr/_shared.py +0 -0
  68. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/stolen/ifaddr/_win32.py +0 -0
  69. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/stolen/qrcodegen.py +0 -0
  70. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/stolen/surrogateescape.py +0 -0
  71. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/sutil.py +0 -0
  72. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/szip.py +0 -0
  73. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/tcpsrv.py +0 -0
  74. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/th_cli.py +0 -0
  75. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/th_srv.py +0 -0
  76. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/u2idx.py +0 -0
  77. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/util.py +0 -0
  78. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/a/__init__.py +0 -0
  79. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/a/partyfuse.py +0 -0
  80. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/a/webdav-cfg.bat +0 -0
  81. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/baguettebox.js.gz +0 -0
  82. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/browser.html +0 -0
  83. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/browser2.html +0 -0
  84. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/cf.html +0 -0
  85. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/dbg-audio.js.gz +0 -0
  86. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/dd/2.png +0 -0
  87. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/dd/3.png +0 -0
  88. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/dd/4.png +0 -0
  89. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/dd/5.png +0 -0
  90. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/dd/__init__.py +0 -0
  91. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/deps/__init__.py +0 -0
  92. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/deps/busy.mp3.gz +0 -0
  93. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/deps/easymde.css.gz +0 -0
  94. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/deps/easymde.js.gz +0 -0
  95. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/deps/marked.js.gz +0 -0
  96. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/deps/mini-fa.css.gz +0 -0
  97. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/deps/mini-fa.woff +0 -0
  98. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/deps/prism.css.gz +0 -0
  99. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/deps/prism.js.gz +0 -0
  100. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/deps/prismd.css.gz +0 -0
  101. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/deps/scp.woff2 +0 -0
  102. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/deps/sha512.ac.js.gz +0 -0
  103. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/deps/sha512.hw.js.gz +0 -0
  104. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/md.css.gz +0 -0
  105. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/md.html +0 -0
  106. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/md.js.gz +0 -0
  107. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/md2.css.gz +0 -0
  108. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/md2.js.gz +0 -0
  109. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/mde.css.gz +0 -0
  110. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/mde.html +0 -0
  111. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/mde.js.gz +0 -0
  112. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/msg.css.gz +0 -0
  113. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/msg.html +0 -0
  114. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/svcs.html +0 -0
  115. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/svcs.js.gz +0 -0
  116. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/up2k.js.gz +0 -0
  117. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/w.hash.js.gz +0 -0
  118. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty.egg-info/SOURCES.txt +0 -0
  119. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty.egg-info/dependency_links.txt +0 -0
  120. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty.egg-info/entry_points.txt +0 -0
  121. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty.egg-info/requires.txt +0 -0
  122. {copyparty-1.14.1 → copyparty-1.14.3}/copyparty.egg-info/top_level.txt +0 -0
  123. {copyparty-1.14.1 → copyparty-1.14.3}/pyproject.toml +0 -0
  124. {copyparty-1.14.1 → copyparty-1.14.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: copyparty
3
- Version: 1.14.1
3
+ Version: 1.14.3
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
@@ -52,7 +52,9 @@ Requires-Dist: partftpy>=0.4.0; extra == "tftpd"
52
52
  Provides-Extra: pwhash
53
53
  Requires-Dist: argon2-cffi; extra == "pwhash"
54
54
 
55
- # 💾🎉 copyparty
55
+ <img src="docs/logo.svg" width="250" align="right"/>
56
+
57
+ ### 💾🎉 copyparty
56
58
 
57
59
  turn almost any device into a file server with resumable uploads/downloads using [*any*](#browser-support) web browser
58
60
 
@@ -804,14 +806,16 @@ you can move files across browser tabs (cut in one tab, paste in another)
804
806
 
805
807
  share a file or folder by creating a temporary link
806
808
 
807
- when enabled in the server settings (`--shr`), click the bottom-right `share` button to share the folder you're currently in, or select a file first to share only that file
809
+ when enabled in the server settings (`--shr`), click the bottom-right `share` button to share the folder you're currently in, or alternatively:
810
+ * select a folder first to share that folder instead
811
+ * select one or more files to share only those files
808
812
 
809
813
  this feature was made with [identity providers](#identity-providers) in mind -- configure your reverseproxy to skip the IdP's access-control for a given URL prefix and use that to safely share specific files/folders sans the usual auth checks
810
814
 
811
815
  when creating a share, the creator can choose any of the following options:
812
816
 
813
817
  * password-protection
814
- * expire after a certain time
818
+ * expire after a certain time; `0` or blank means infinite
815
819
  * allow visitors to upload (if the user who creates the share has write-access)
816
820
 
817
821
  semi-intentional limitations:
@@ -822,10 +826,17 @@ semi-intentional limitations:
822
826
  * when linking something to discord (for example) it'll get accessed by their scraper and that would count as a hit
823
827
  * browsers wouldn't be able to resume a broken download unless the requester's IP gets allowlisted for X minutes (ref. tricky)
824
828
 
825
- the links are created inside a specific toplevel folder which must be specified with server-config `--shr`, for example `--shr /share/` (this also enables the feature)
829
+ specify `--shr /foobar` to enable this feature; a toplevel virtual folder named `foobar` is then created, and that's where all the shares will be served from
830
+
831
+ * you can name it whatever, `foobar` is just an example
832
+ * if you're using config files, put `shr: /foobar` inside the `[global]` section instead
826
833
 
827
834
  users can delete their own shares in the controlpanel, and a list of privileged users (`--shr-adm`) are allowed to see and/or delet any share on the server
828
835
 
836
+ after a share has expired, it remains visible in the controlpanel for `--shr-rt` minutes (default is 1 day), and the owner can revive it by extending the expiration time there
837
+
838
+ **security note:** using this feature does not mean that you can skip the [accounts and volumes](#accounts-and-volumes) section -- you still need to restrict access to volumes that you do not intend to share with unauthenticated users! it is not sufficient to use rules in the reverseproxy to restrict access to just the `/share` folder.
839
+
829
840
 
830
841
  ## batch rename
831
842
 
@@ -1,4 +1,6 @@
1
- # 💾🎉 copyparty
1
+ <img src="docs/logo.svg" width="250" align="right"/>
2
+
3
+ ### 💾🎉 copyparty
2
4
 
3
5
  turn almost any device into a file server with resumable uploads/downloads using [*any*](#browser-support) web browser
4
6
 
@@ -750,14 +752,16 @@ you can move files across browser tabs (cut in one tab, paste in another)
750
752
 
751
753
  share a file or folder by creating a temporary link
752
754
 
753
- when enabled in the server settings (`--shr`), click the bottom-right `share` button to share the folder you're currently in, or select a file first to share only that file
755
+ when enabled in the server settings (`--shr`), click the bottom-right `share` button to share the folder you're currently in, or alternatively:
756
+ * select a folder first to share that folder instead
757
+ * select one or more files to share only those files
754
758
 
755
759
  this feature was made with [identity providers](#identity-providers) in mind -- configure your reverseproxy to skip the IdP's access-control for a given URL prefix and use that to safely share specific files/folders sans the usual auth checks
756
760
 
757
761
  when creating a share, the creator can choose any of the following options:
758
762
 
759
763
  * password-protection
760
- * expire after a certain time
764
+ * expire after a certain time; `0` or blank means infinite
761
765
  * allow visitors to upload (if the user who creates the share has write-access)
762
766
 
763
767
  semi-intentional limitations:
@@ -768,10 +772,17 @@ semi-intentional limitations:
768
772
  * when linking something to discord (for example) it'll get accessed by their scraper and that would count as a hit
769
773
  * browsers wouldn't be able to resume a broken download unless the requester's IP gets allowlisted for X minutes (ref. tricky)
770
774
 
771
- the links are created inside a specific toplevel folder which must be specified with server-config `--shr`, for example `--shr /share/` (this also enables the feature)
775
+ specify `--shr /foobar` to enable this feature; a toplevel virtual folder named `foobar` is then created, and that's where all the shares will be served from
776
+
777
+ * you can name it whatever, `foobar` is just an example
778
+ * if you're using config files, put `shr: /foobar` inside the `[global]` section instead
772
779
 
773
780
  users can delete their own shares in the controlpanel, and a list of privileged users (`--shr-adm`) are allowed to see and/or delet any share on the server
774
781
 
782
+ after a share has expired, it remains visible in the controlpanel for `--shr-rt` minutes (default is 1 day), and the owner can revive it by extending the expiration time there
783
+
784
+ **security note:** using this feature does not mean that you can skip the [accounts and volumes](#accounts-and-volumes) section -- you still need to restrict access to volumes that you do not intend to share with unauthenticated users! it is not sufficient to use rules in the reverseproxy to restrict access to just the `/share` folder.
785
+
775
786
 
776
787
  ## batch rename
777
788
 
@@ -969,9 +969,10 @@ def add_fs(ap):
969
969
  def add_share(ap):
970
970
  db_path = os.path.join(E.cfg, "shares.db")
971
971
  ap2 = ap.add_argument_group('share-url options')
972
- ap2.add_argument("--shr", metavar="URL", default="", help="base url for shared files, for example [\033[32m/share\033[0m] (must be a toplevel subfolder)")
973
- ap2.add_argument("--shr-db", metavar="PATH", default=db_path, help="database to store shares in")
974
- ap2.add_argument("--shr-adm", metavar="U,U", default="", help="comma-separated list of users allowed to view/delete any share")
972
+ ap2.add_argument("--shr", metavar="DIR", type=u, default="", help="toplevel virtual folder for shared files/folders, for example [\033[32m/share\033[0m]")
973
+ ap2.add_argument("--shr-db", metavar="FILE", type=u, default=db_path, help="database to store shares in")
974
+ ap2.add_argument("--shr-adm", metavar="U,U", type=u, default="", help="comma-separated list of users allowed to view/delete any share")
975
+ 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")
975
976
  ap2.add_argument("--shr-v", action="store_true", help="debug")
976
977
 
977
978
 
@@ -1406,7 +1407,7 @@ def add_ui(ap, retry):
1406
1407
  ap2 = ap.add_argument_group('ui options')
1407
1408
  ap2.add_argument("--grid", action="store_true", help="show grid/thumbnails by default (volflag=grid)")
1408
1409
  ap2.add_argument("--gsel", action="store_true", help="select files in grid by ctrl-click (volflag=gsel)")
1409
- ap2.add_argument("--lang", metavar="LANG", type=u, default="eng", help="language; one of the following: \033[32meng nor\033[0m")
1410
+ ap2.add_argument("--lang", metavar="LANG", type=u, default="eng", help="language; one of the following: \033[32meng nor chi\033[0m")
1410
1411
  ap2.add_argument("--theme", metavar="NUM", type=int, default=0, help="default theme to use (0..7)")
1411
1412
  ap2.add_argument("--themes", metavar="NUM", type=int, default=8, help="number of themes installed")
1412
1413
  ap2.add_argument("--au-vol", metavar="0-100", type=int, default=50, choices=range(0, 101), help="default audio/video volume percent")
@@ -1,8 +1,8 @@
1
1
  # coding: utf-8
2
2
 
3
- VERSION = (1, 14, 1)
3
+ VERSION = (1, 14, 3)
4
4
  CODENAME = "one step forward"
5
- BUILD_DT = (2024, 8, 19)
5
+ BUILD_DT = (2024, 8, 30)
6
6
 
7
7
  S_VERSION = ".".join(map(str, VERSION))
8
8
  S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)
@@ -35,6 +35,7 @@ from .util import (
35
35
  odfusion,
36
36
  relchk,
37
37
  statdir,
38
+ ub64enc,
38
39
  uncyg,
39
40
  undot,
40
41
  unhumanize,
@@ -337,6 +338,7 @@ class VFS(object):
337
338
  self.dbv = None # closest full/non-jump parent
338
339
  self.lim = None # upload limits; only set for dbv
339
340
  self.shr_src = None # source vfs+rem of a share
341
+ self.shr_files = set() # filenames to include from shr_src
340
342
  self.aread = {}
341
343
  self.awrite = {}
342
344
  self.amove = {}
@@ -362,6 +364,7 @@ class VFS(object):
362
364
  self.all_vps = []
363
365
 
364
366
  self.get_dbv = self._get_dbv
367
+ self.ls = self._ls
365
368
 
366
369
  def __repr__(self) :
367
370
  return "VFS(%s)" % (
@@ -558,7 +561,26 @@ class VFS(object):
558
561
  ad, fn = os.path.split(ap)
559
562
  return os.path.join(absreal(ad), fn)
560
563
 
561
- def ls(
564
+ def _ls_nope(
565
+ self, *a, **ka
566
+ ) :
567
+ raise Pebkac(500, "nope.avi")
568
+
569
+ def _ls_shr(
570
+ self,
571
+ rem ,
572
+ uname ,
573
+ scandir ,
574
+ permsets ,
575
+ lstat = False,
576
+ ) :
577
+ """replaces _ls for certain shares (single-file, or file selection)"""
578
+ vn, rem = self.shr_src # type: ignore
579
+ abspath, real, _ = vn.ls(rem, "\n", scandir, permsets, lstat)
580
+ real = [x for x in real if os.path.basename(x[0]) in self.shr_files]
581
+ return abspath, real, {}
582
+
583
+ def _ls(
562
584
  self,
563
585
  rem ,
564
586
  uname ,
@@ -1501,14 +1523,14 @@ class AuthSrv(object):
1501
1523
  import sqlite3
1502
1524
 
1503
1525
  shv = VFS(self.log_func, "", shr, AXS(), {"d2d": True})
1504
- par = vfs.all_vols[""]
1505
1526
 
1506
1527
  db_path = self.args.shr_db
1507
1528
  db = sqlite3.connect(db_path)
1508
1529
  cur = db.cursor()
1530
+ cur2 = db.cursor()
1509
1531
  now = time.time()
1510
1532
  for row in cur.execute("select * from sh"):
1511
- s_k, s_pw, s_vp, s_pr, s_st, s_un, s_t0, s_t1 = row
1533
+ s_k, s_pw, s_vp, s_pr, s_nf, s_un, s_t0, s_t1 = row
1512
1534
  if s_t1 and s_t1 < now:
1513
1535
  continue
1514
1536
 
@@ -1517,7 +1539,10 @@ class AuthSrv(object):
1517
1539
  self.log(t % (s_pr, s_k, s_un, s_vp))
1518
1540
 
1519
1541
  if s_pw:
1520
- sun = "s_%s" % (s_k,)
1542
+ # gotta reuse the "account" for all shares with this pw,
1543
+ # so do a light scramble as this appears in the web-ui
1544
+ zs = ub64enc(hashlib.sha512(s_pw.encode("utf-8")).digest())[4:16]
1545
+ sun = "s_%s" % (zs.decode("utf-8"),)
1521
1546
  acct[sun] = s_pw
1522
1547
  else:
1523
1548
  sun = "*"
@@ -1532,13 +1557,14 @@ class AuthSrv(object):
1532
1557
  # don't know the abspath yet + wanna ensure the user
1533
1558
  # still has the privs they granted, so nullmap it
1534
1559
  shv.nodes[s_k] = VFS(
1535
- self.log_func, "", "%s/%s" % (shr, s_k), s_axs, par.flags.copy()
1560
+ self.log_func, "", "%s/%s" % (shr, s_k), s_axs, shv.flags.copy()
1536
1561
  )
1537
1562
 
1538
1563
  vfs.nodes[shr] = vfs.all_vols[shr] = shv
1539
1564
  for vol in shv.nodes.values():
1540
1565
  vfs.all_vols[vol.vpath] = vol
1541
1566
  vol.get_dbv = vol._get_share_src
1567
+ vol.ls = vol._ls_nope
1542
1568
 
1543
1569
  zss = set(acct)
1544
1570
  zss.update(self.idp_accs)
@@ -2048,6 +2074,9 @@ class AuthSrv(object):
2048
2074
  if not self.warn_anonwrite or verbosity < 5:
2049
2075
  break
2050
2076
 
2077
+ if enshare and (zv.vpath == shr or zv.vpath.startswith(shrs)):
2078
+ continue
2079
+
2051
2080
  t += '\n\033[36m"/{}" \033[33m{}\033[0m'.format(zv.vpath, zv.realpath)
2052
2081
  for txt, attr in [
2053
2082
  [" read", "uread"],
@@ -2154,10 +2183,9 @@ class AuthSrv(object):
2154
2183
  if x != shr and not x.startswith(shrs)
2155
2184
  }
2156
2185
 
2157
- assert cur # type: ignore
2158
- assert shv # type: ignore
2186
+ assert db and cur and cur2 and shv # type: ignore
2159
2187
  for row in cur.execute("select * from sh"):
2160
- s_k, s_pw, s_vp, s_pr, s_st, s_un, s_t0, s_t1 = row
2188
+ s_k, s_pw, s_vp, s_pr, s_nf, s_un, s_t0, s_t1 = row
2161
2189
  shn = shv.nodes.get(s_k, None)
2162
2190
  if not shn:
2163
2191
  continue
@@ -2172,6 +2200,17 @@ class AuthSrv(object):
2172
2200
  shv.nodes.pop(s_k)
2173
2201
  continue
2174
2202
 
2203
+ fns = []
2204
+ if s_nf:
2205
+ q = "select vp from sf where k = ?"
2206
+ for (s_fn,) in cur2.execute(q, (s_k,)):
2207
+ fns.append(s_fn)
2208
+
2209
+ shn.shr_files = set(fns)
2210
+ shn.ls = shn._ls_shr
2211
+ else:
2212
+ shn.ls = shn._ls
2213
+
2175
2214
  shn.shr_src = (s_vfs, s_rem)
2176
2215
  shn.realpath = s_vfs.canonical(s_rem)
2177
2216
 
@@ -2191,6 +2230,10 @@ class AuthSrv(object):
2191
2230
  # hide subvolume
2192
2231
  vn.nodes[zs] = VFS(self.log_func, "", "", AXS(), {})
2193
2232
 
2233
+ cur2.close()
2234
+ cur.close()
2235
+ db.close()
2236
+
2194
2237
  def chpw(self, broker , uname, pw) :
2195
2238
  if not self.args.chpw:
2196
2239
  return False, "feature disabled in server config"
@@ -1607,8 +1607,8 @@ class HttpCli(object):
1607
1607
  if "delete" in self.uparam:
1608
1608
  return self.handle_rm([])
1609
1609
 
1610
- if "unshare" in self.uparam:
1611
- return self.handle_unshare()
1610
+ if "eshare" in self.uparam:
1611
+ return self.handle_eshare()
1612
1612
 
1613
1613
  if "application/octet-stream" in ctype:
1614
1614
  return self.handle_post_binary()
@@ -3262,7 +3262,8 @@ class HttpCli(object):
3262
3262
  raise Exception("not found in registry")
3263
3263
  self.pipes.set(req_path, job)
3264
3264
  except Exception as ex:
3265
- self.log("will not pipe [%s]; %s" % (ap_data, ex), 6)
3265
+ if getattr(ex, "errno", 0) != errno.ENOENT:
3266
+ self.log("will not pipe [%s]; %s" % (ap_data, ex), 6)
3266
3267
  ptop = None
3267
3268
 
3268
3269
  #
@@ -3954,6 +3955,7 @@ class HttpCli(object):
3954
3955
  rvol=rvol,
3955
3956
  wvol=wvol,
3956
3957
  avol=avol,
3958
+ in_shr=self.args.shr and self.vpath.startswith(self.args.shr[1:]),
3957
3959
  vstate=vstate,
3958
3960
  scanning=vs["scanning"],
3959
3961
  hashq=vs["hashq"],
@@ -4002,10 +4004,10 @@ class HttpCli(object):
4002
4004
  def tx_404(self, is_403 = False) :
4003
4005
  rc = 404
4004
4006
  if self.args.vague_403:
4005
- t = '<h1 id="n">404 not found &nbsp;┐( ´ -`)┌</h1><p id="o">or maybe you don\'t have access -- try logging in or <a href="{}/?h">go home</a></p>'
4006
- pt = "404 not found ┐( ´ -`)┌ (or maybe you don't have access -- try logging in)"
4007
+ t = '<h1 id="n">404 not found &nbsp;┐( ´ -`)┌</h1><p id="o">or maybe you don\'t have access -- try a password or <a href="{}/?h">go home</a></p>'
4008
+ pt = "404 not found ┐( ´ -`)┌ (or maybe you don't have access -- try a password)"
4007
4009
  elif is_403:
4008
- t = '<h1 id="p">403 forbiddena &nbsp;~┻━┻</h1><p id="q">you\'ll have to log in or <a href="{}/?h">go home</a></p>'
4010
+ t = '<h1 id="p">403 forbiddena &nbsp;~┻━┻</h1><p id="q">use a password or <a href="{}/?h">go home</a></p>'
4009
4011
  pt = "403 forbiddena ~┻━┻ (you'll have to log in)"
4010
4012
  rc = 403
4011
4013
  else:
@@ -4022,7 +4024,8 @@ class HttpCli(object):
4022
4024
 
4023
4025
  t = t.format(self.args.SR)
4024
4026
  qv = quotep(self.vpaths) + self.ourlq()
4025
- html = self.j2s("splash", this=self, qvpath=qv, msg=t)
4027
+ in_shr = self.args.shr and self.vpath.startswith(self.args.shr[1:])
4028
+ html = self.j2s("splash", this=self, qvpath=qv, in_shr=in_shr, msg=t)
4026
4029
  self.reply(html.encode("utf-8"), status=rc)
4027
4030
  return True
4028
4031
 
@@ -4297,7 +4300,7 @@ class HttpCli(object):
4297
4300
  self.reply(html.encode("utf-8"), status=200)
4298
4301
  return True
4299
4302
 
4300
- def handle_unshare(self) :
4303
+ def handle_eshare(self) :
4301
4304
  idx = self.conn.get_u2idx()
4302
4305
  if not idx or not hasattr(idx, "p_end"):
4303
4306
  if not HAVE_SQLITE3:
@@ -4305,7 +4308,7 @@ class HttpCli(object):
4305
4308
  raise Pebkac(500, "server busy, cannot create share; please retry in a bit")
4306
4309
 
4307
4310
  if self.args.shr_v:
4308
- self.log("handle_unshare: " + self.req)
4311
+ self.log("handle_eshare: " + self.req)
4309
4312
 
4310
4313
  cur = idx.get_shr()
4311
4314
  if not cur:
@@ -4313,18 +4316,36 @@ class HttpCli(object):
4313
4316
 
4314
4317
  skey = self.vpath.split("/")[-1]
4315
4318
 
4316
- uns = cur.execute("select un from sh where k = ?", (skey,)).fetchall()
4317
- un = uns[0][0] if uns and uns[0] else ""
4319
+ rows = cur.execute("select un, t1 from sh where k = ?", (skey,)).fetchall()
4320
+ un = rows[0][0] if rows and rows[0] else ""
4318
4321
 
4319
4322
  if not un:
4320
4323
  raise Pebkac(400, "that sharekey didn't match anything")
4321
4324
 
4325
+ expiry = rows[0][1]
4326
+
4322
4327
  if un != self.uname and self.uname != self.args.shr_adm:
4323
4328
  t = "your username (%r) does not match the sharekey's owner (%r) and you're not admin"
4324
4329
  raise Pebkac(400, t % (self.uname, un))
4325
4330
 
4326
- cur.execute("delete from sh where k = ?", (skey,))
4331
+ reload = False
4332
+ act = self.uparam["eshare"]
4333
+ if act == "rm":
4334
+ cur.execute("delete from sh where k = ?", (skey,))
4335
+ if skey in self.asrv.vfs.nodes[self.args.shr.strip("/")].nodes:
4336
+ reload = True
4337
+ else:
4338
+ now = time.time()
4339
+ if expiry < now:
4340
+ expiry = now
4341
+ reload = True
4342
+ expiry += int(act) * 60
4343
+ cur.execute("update sh set t1 = ? where k = ?", (expiry, skey))
4344
+
4327
4345
  cur.connection.commit()
4346
+ if reload:
4347
+ self.conn.hsrv.broker.ask("_reload_blocking", False, False).get()
4348
+ self.conn.hsrv.broker.ask("up2k.wake_rescanner").get()
4328
4349
 
4329
4350
  self.redirect(self.args.SRS + "?shares")
4330
4351
  return True
@@ -4340,11 +4361,31 @@ class HttpCli(object):
4340
4361
  self.log("handle_share: " + json.dumps(req, indent=4))
4341
4362
 
4342
4363
  skey = req["k"]
4343
- vp = req["vp"].strip("/")
4364
+ vps = req["vp"]
4365
+ fns = []
4366
+ if len(vps) == 1:
4367
+ vp = vps[0]
4368
+ if not vp.endswith("/"):
4369
+ vp, zs = vp.rsplit("/", 1)
4370
+ fns = [zs]
4371
+ else:
4372
+ for zs in vps:
4373
+ if zs.endswith("/"):
4374
+ t = "you cannot select more than one folder, or mix flies and folders in one selection"
4375
+ raise Pebkac(400, t)
4376
+ vp = vps[0].rsplit("/", 1)[0]
4377
+ for zs in vps:
4378
+ vp2, fn = zs.rsplit("/", 1)
4379
+ fns.append(fn)
4380
+ if vp != vp2:
4381
+ t = "mismatching base paths in selection:\n [%s]\n [%s]"
4382
+ raise Pebkac(400, t % (vp, vp2))
4383
+
4384
+ vp = vp.strip("/")
4344
4385
  if self.is_vproxied and (vp == self.args.R or vp.startswith(self.args.RS)):
4345
4386
  vp = vp[len(self.args.RS) :]
4346
4387
 
4347
- m = re.search(r"([^0-9a-zA-Z_\.-]|\.\.|^\.)", skey)
4388
+ m = re.search(r"([^0-9a-zA-Z_-])", skey)
4348
4389
  if m:
4349
4390
  raise Pebkac(400, "sharekey has illegal character [%s]" % (m[1],))
4350
4391
 
@@ -4371,29 +4412,41 @@ class HttpCli(object):
4371
4412
  except:
4372
4413
  raise Pebkac(400, "you dont have all the perms you tried to grant")
4373
4414
 
4374
- ap = vfs.canonical(rem)
4375
- st = bos.stat(ap)
4376
- ist = 2 if stat.S_ISDIR(st.st_mode) else 1
4415
+ ap, reals, _ = vfs.ls(
4416
+ rem, self.uname, not self.args.no_scandir, [[s_rd, s_wr, s_mv, s_del]]
4417
+ )
4418
+ rfns = set([x[0] for x in reals])
4419
+ for fn in fns:
4420
+ if fn not in rfns:
4421
+ raise Pebkac(400, "selected file not found on disk: [%s]" % (fn,))
4377
4422
 
4378
4423
  pw = req.get("pw") or ""
4379
4424
  now = int(time.time())
4380
4425
  sexp = req["exp"]
4381
- exp = now + int(sexp) * 60 if sexp else 0
4426
+ exp = int(sexp) if sexp else 0
4427
+ exp = now + exp * 60 if exp else 0
4382
4428
  pr = "".join(zc for zc, zb in zip("rwmd", (s_rd, s_wr, s_mv, s_del)) if zb)
4383
4429
 
4384
4430
  q = "insert into sh values (?,?,?,?,?,?,?,?)"
4385
- cur.execute(q, (skey, pw, vp, pr, ist, self.uname, now, exp))
4386
- cur.connection.commit()
4431
+ cur.execute(q, (skey, pw, vp, pr, len(fns), self.uname, now, exp))
4432
+
4433
+ q = "insert into sf values (?,?)"
4434
+ for fn in fns:
4435
+ cur.execute(q, (skey, fn))
4387
4436
 
4437
+ cur.connection.commit()
4388
4438
  self.conn.hsrv.broker.ask("_reload_blocking", False, False).get()
4389
4439
  self.conn.hsrv.broker.ask("up2k.wake_rescanner").get()
4390
4440
 
4391
- surl = "%s://%s%s%s%s" % (
4441
+ fn = quotep(fns[0]) if len(fns) == 1 else ""
4442
+
4443
+ surl = "created share: %s://%s%s%s%s/%s" % (
4392
4444
  "https" if self.is_https else "http",
4393
4445
  self.host,
4394
4446
  self.args.SR,
4395
4447
  self.args.shr,
4396
4448
  skey,
4449
+ fn,
4397
4450
  )
4398
4451
  self.loud_reply(surl, status=201)
4399
4452
  return True
@@ -100,6 +100,7 @@ class SvcHub(object):
100
100
  self.no_ansi = args.no_ansi
101
101
  self.logf = None
102
102
  self.logf_base_fn = ""
103
+ self.is_dut = False # running in unittest; always False
103
104
  self.stop_req = False
104
105
  self.stopping = False
105
106
  self.stopped = False
@@ -370,11 +371,18 @@ class SvcHub(object):
370
371
 
371
372
  import sqlite3
372
373
 
373
- al.shr = "/%s/" % (al.shr.strip("/"))
374
+ al.shr = al.shr.strip("/")
375
+ if "/" in al.shr or not al.shr:
376
+ t = "config error: --shr must be the name of a virtual toplevel directory to put shares inside"
377
+ self.log("root", t, 1)
378
+ raise Exception(t)
379
+
380
+ al.shr = "/%s/" % (al.shr,)
374
381
 
375
382
  create = True
383
+ modified = False
376
384
  db_path = self.args.shr_db
377
- self.log("root", "initializing shares-db %s" % (db_path,))
385
+ self.log("root", "opening shares-db %s" % (db_path,))
378
386
  for n in range(2):
379
387
  try:
380
388
  db = sqlite3.connect(db_path)
@@ -400,18 +408,43 @@ class SvcHub(object):
400
408
  pass
401
409
  os.unlink(db_path)
402
410
 
411
+ sch1 = [
412
+ r"create table kv (k text, v int)",
413
+ r"create table sh (k text, pw text, vp text, pr text, st int, un text, t0 int, t1 int)",
414
+ # sharekey, password, src, perms, numFiles, owner, created, expires
415
+ ]
416
+ sch2 = [
417
+ r"create table sf (k text, vp text)",
418
+ r"create index sf_k on sf(k)",
419
+ r"create index sh_k on sh(k)",
420
+ r"create index sh_t1 on sh(t1)",
421
+ ]
422
+
403
423
  assert db # type: ignore
404
424
  assert cur # type: ignore
405
425
  if create:
426
+ dver = 2
427
+ modified = True
428
+ for cmd in sch1 + sch2:
429
+ cur.execute(cmd)
430
+ self.log("root", "created new shares-db")
431
+ else:
432
+ (dver,) = cur.execute("select v from kv where k = 'sver'").fetchall()[0]
433
+
434
+ if dver == 1:
435
+ modified = True
436
+ for cmd in sch2:
437
+ cur.execute(cmd)
438
+ cur.execute("update sh set st = 0")
439
+ self.log("root", "shares-db schema upgrade ok")
440
+
441
+ if modified:
406
442
  for cmd in [
407
- # sharekey, password, src, perms, type, owner, created, expires
408
- r"create table sh (k text, pw text, vp text, pr text, st int, un text, t0 int, t1 int)",
409
- r"create table kv (k text, v int)",
410
- r"insert into kv values ('sver', {})".format(1),
443
+ r"delete from kv where k = 'sver'",
444
+ r"insert into kv values ('sver', %d)" % (2,),
411
445
  ]:
412
446
  cur.execute(cmd)
413
447
  db.commit()
414
- self.log("root", "created new shares-db")
415
448
 
416
449
  cur.close()
417
450
  db.close()
@@ -400,7 +400,7 @@ class Tftpd(object):
400
400
  bos.stat(ap)
401
401
  return True
402
402
  except:
403
- return False
403
+ return vpath == "/"
404
404
 
405
405
  def _p_isdir(self, vpath ) :
406
406
  try:
@@ -408,7 +408,7 @@ class Tftpd(object):
408
408
  ret = stat.S_ISDIR(st.st_mode)
409
409
  return ret
410
410
  except:
411
- return False
411
+ return vpath == "/"
412
412
 
413
413
  def _hook(self, *a , **ka ) :
414
414
  src = inspect.currentframe().f_back.f_code.co_name