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.
- {copyparty-1.14.1 → copyparty-1.14.3}/PKG-INFO +16 -5
- {copyparty-1.14.1 → copyparty-1.14.3}/README.md +15 -4
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/__main__.py +5 -4
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/__version__.py +2 -2
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/authsrv.py +51 -8
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/httpcli.py +74 -21
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/svchub.py +40 -7
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/tftpd.py +2 -2
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/up2k.py +65 -12
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/a/u2c.py +5 -3
- copyparty-1.14.3/copyparty/web/browser.css.gz +0 -0
- copyparty-1.14.3/copyparty/web/browser.js.gz +0 -0
- copyparty-1.14.3/copyparty/web/shares.css.gz +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/shares.html +10 -8
- copyparty-1.14.3/copyparty/web/shares.js.gz +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/splash.css.gz +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/splash.html +33 -15
- copyparty-1.14.3/copyparty/web/splash.js.gz +0 -0
- copyparty-1.14.3/copyparty/web/ui.css.gz +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/util.js.gz +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty.egg-info/PKG-INFO +16 -5
- copyparty-1.14.1/copyparty/web/browser.css.gz +0 -0
- copyparty-1.14.1/copyparty/web/browser.js.gz +0 -0
- copyparty-1.14.1/copyparty/web/shares.css.gz +0 -0
- copyparty-1.14.1/copyparty/web/shares.js.gz +0 -0
- copyparty-1.14.1/copyparty/web/splash.js.gz +0 -0
- copyparty-1.14.1/copyparty/web/ui.css.gz +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/LICENSE +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/__init__.py +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/bos/__init__.py +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/bos/bos.py +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/bos/path.py +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/broker_mp.py +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/broker_mpw.py +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/broker_thr.py +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/broker_util.py +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/cert.py +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/cfg.py +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/dxml.py +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/fsutil.py +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/ftpd.py +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/httpconn.py +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/httpsrv.py +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/ico.py +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/mdns.py +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/metrics.py +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/mtag.py +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/multicast.py +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/pwhash.py +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/res/COPYING.txt +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/res/__init__.py +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/res/insecure.pem +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/smbd.py +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/ssdp.py +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/star.py +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/stolen/__init__.py +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/stolen/dnslib/__init__.py +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/stolen/dnslib/bimap.py +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/stolen/dnslib/bit.py +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/stolen/dnslib/buffer.py +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/stolen/dnslib/dns.py +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/stolen/dnslib/label.py +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/stolen/dnslib/lex.py +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/stolen/dnslib/ranges.py +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/stolen/ifaddr/__init__.py +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/stolen/ifaddr/_posix.py +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/stolen/ifaddr/_shared.py +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/stolen/ifaddr/_win32.py +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/stolen/qrcodegen.py +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/stolen/surrogateescape.py +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/sutil.py +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/szip.py +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/tcpsrv.py +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/th_cli.py +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/th_srv.py +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/u2idx.py +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/util.py +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/a/__init__.py +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/a/partyfuse.py +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/a/webdav-cfg.bat +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/baguettebox.js.gz +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/browser.html +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/browser2.html +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/cf.html +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/dbg-audio.js.gz +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/dd/2.png +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/dd/3.png +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/dd/4.png +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/dd/5.png +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/dd/__init__.py +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/deps/__init__.py +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/deps/busy.mp3.gz +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/deps/easymde.css.gz +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/deps/easymde.js.gz +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/deps/marked.js.gz +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/deps/mini-fa.css.gz +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/deps/mini-fa.woff +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/deps/prism.css.gz +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/deps/prism.js.gz +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/deps/prismd.css.gz +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/deps/scp.woff2 +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/deps/sha512.ac.js.gz +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/deps/sha512.hw.js.gz +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/md.css.gz +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/md.html +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/md.js.gz +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/md2.css.gz +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/md2.js.gz +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/mde.css.gz +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/mde.html +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/mde.js.gz +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/msg.css.gz +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/msg.html +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/svcs.html +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/svcs.js.gz +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/up2k.js.gz +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty/web/w.hash.js.gz +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty.egg-info/SOURCES.txt +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty.egg-info/dependency_links.txt +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty.egg-info/entry_points.txt +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty.egg-info/requires.txt +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/copyparty.egg-info/top_level.txt +0 -0
- {copyparty-1.14.1 → copyparty-1.14.3}/pyproject.toml +0 -0
- {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.
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
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="
|
973
|
-
ap2.add_argument("--shr-db", metavar="
|
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")
|
@@ -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
|
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,
|
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
|
-
|
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,
|
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,
|
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 "
|
1611
|
-
return self.
|
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
|
-
|
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 ┐( ´ -`)┌</h1><p id="o">or maybe you don\'t have access -- try
|
4006
|
-
pt = "404 not found ┐( ´ -`)┌ (or maybe you don't have access -- try
|
4007
|
+
t = '<h1 id="n">404 not found ┐( ´ -`)┌</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 ~┻━┻</h1><p id="q">
|
4010
|
+
t = '<h1 id="p">403 forbiddena ~┻━┻</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
|
-
|
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
|
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("
|
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
|
-
|
4317
|
-
un =
|
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
|
-
|
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
|
-
|
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_
|
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.
|
4375
|
-
|
4376
|
-
|
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 =
|
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,
|
4386
|
-
|
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
|
-
|
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 =
|
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", "
|
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
|
-
|
408
|
-
r"
|
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
|
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
|
411
|
+
return vpath == "/"
|
412
412
|
|
413
413
|
def _hook(self, *a , **ka ) :
|
414
414
|
src = inspect.currentframe().f_back.f_code.co_name
|