copyparty 1.19.0__py3-none-any.whl → 1.19.2__py3-none-any.whl
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/__main__.py +131 -27
- copyparty/__version__.py +2 -2
- copyparty/authsrv.py +33 -7
- copyparty/cfg.py +7 -1
- copyparty/dxml.py +3 -0
- copyparty/ftpd.py +21 -6
- copyparty/httpcli.py +92 -23
- copyparty/httpsrv.py +6 -0
- copyparty/mdns.py +2 -1
- copyparty/mtag.py +88 -6
- copyparty/svchub.py +76 -5
- copyparty/tcpsrv.py +6 -0
- copyparty/th_cli.py +5 -1
- copyparty/th_srv.py +160 -51
- copyparty/u2idx.py +1 -1
- copyparty/up2k.py +80 -39
- copyparty/util.py +25 -1
- copyparty/web/baguettebox.js.gz +0 -0
- copyparty/web/browser.css.gz +0 -0
- copyparty/web/browser.js.gz +0 -0
- copyparty/web/rups.js.gz +0 -0
- copyparty/web/splash.css.gz +0 -0
- copyparty/web/splash.html +8 -1
- copyparty/web/splash.js.gz +0 -0
- copyparty/web/svcs.html +1 -1
- copyparty/web/up2k.js.gz +0 -0
- copyparty/web/util.js.gz +0 -0
- {copyparty-1.19.0.dist-info → copyparty-1.19.2.dist-info}/METADATA +39 -3
- {copyparty-1.19.0.dist-info → copyparty-1.19.2.dist-info}/RECORD +33 -33
- {copyparty-1.19.0.dist-info → copyparty-1.19.2.dist-info}/WHEEL +0 -0
- {copyparty-1.19.0.dist-info → copyparty-1.19.2.dist-info}/entry_points.txt +0 -0
- {copyparty-1.19.0.dist-info → copyparty-1.19.2.dist-info}/licenses/LICENSE +0 -0
- {copyparty-1.19.0.dist-info → copyparty-1.19.2.dist-info}/top_level.txt +0 -0
copyparty/__main__.py
CHANGED
@@ -428,6 +428,40 @@ def args_from_cfg(cfg_path ) :
|
|
428
428
|
return ret
|
429
429
|
|
430
430
|
|
431
|
+
def expand_cfg(argv) :
|
432
|
+
if CFG_DEF:
|
433
|
+
supp = args_from_cfg(CFG_DEF[0])
|
434
|
+
argv = supp + argv
|
435
|
+
|
436
|
+
n = spins = 0
|
437
|
+
while n < len(argv):
|
438
|
+
if not n:
|
439
|
+
if spins % 1000 == 999:
|
440
|
+
t = "still expanding config files... giving up after %d more"
|
441
|
+
print(t % (9999 - spins))
|
442
|
+
if spins > 9999:
|
443
|
+
t = "got stuck expanding config files; do you have a config-file which imports itself? this is where I gave up:\n%r"
|
444
|
+
raise Exception(t % (argv[:1000]))
|
445
|
+
v1 = argv[n]
|
446
|
+
v1v = v1[2:].lstrip("=")
|
447
|
+
try:
|
448
|
+
v2 = argv[n + 1]
|
449
|
+
except:
|
450
|
+
v2 = ""
|
451
|
+
|
452
|
+
if v1 == "-c" and v2 and os.path.isfile(v2):
|
453
|
+
argv = argv[:n] + args_from_cfg(v2) + argv[n + 2 :]
|
454
|
+
spins += 1
|
455
|
+
n = 0
|
456
|
+
elif v1.startswith("-c") and v1v and os.path.isfile(v1v):
|
457
|
+
argv = argv[:n] + args_from_cfg(v1v) + argv[n + 1 :]
|
458
|
+
spins += 1
|
459
|
+
n = 0
|
460
|
+
else:
|
461
|
+
n += 1
|
462
|
+
return argv
|
463
|
+
|
464
|
+
|
431
465
|
def sighandler(sig = None, frame = None) :
|
432
466
|
msg = [""] * 5
|
433
467
|
for th in threading.enumerate():
|
@@ -601,8 +635,41 @@ def get_sects():
|
|
601
635
|
if no accounts or volumes are configured,
|
602
636
|
current folder will be read/write for everyone
|
603
637
|
|
638
|
+
the group @acct will always have every user with an account
|
639
|
+
(the name of that group can be changed with --grp-all)
|
640
|
+
|
604
641
|
consider the config file for more flexible account/volume management,
|
605
642
|
including dynamic reload at runtime (and being more readable w)
|
643
|
+
|
644
|
+
see \033[32m--help-auth\033[0m for ways to provide the password in requests;
|
645
|
+
see \033[32m--help-idp\033[0m for replacing it with SSO and auth-middlewares
|
646
|
+
"""
|
647
|
+
),
|
648
|
+
],
|
649
|
+
[
|
650
|
+
"auth",
|
651
|
+
"how to login from a client",
|
652
|
+
dedent(
|
653
|
+
"""
|
654
|
+
different ways to provide the password so you become authenticated:
|
655
|
+
|
656
|
+
login with the ui:
|
657
|
+
go to \033[36mhttp://127.0.0.1:3923/?h\033[0m and login there
|
658
|
+
|
659
|
+
send the password in the '\033[36mPW\033[0m' http-header:
|
660
|
+
\033[36mPW: \033[35mhunter2\033[0m
|
661
|
+
or if you have \033[33m--accounts\033[0m enabled,
|
662
|
+
\033[36mPW: \033[35med:hunter2\033[0m
|
663
|
+
|
664
|
+
send the password in the URL itself:
|
665
|
+
\033[36mhttp://127.0.0.1:3923/\033[35m?pw=hunter2\033[0m
|
666
|
+
or if you have \033[33m--accounts\033[0m enabled,
|
667
|
+
\033[36mhttp://127.0.0.1:3923/\033[35m?pw=ed:hunter2\033[0m
|
668
|
+
|
669
|
+
use basic-authentication:
|
670
|
+
\033[36mhttp://\033[35med:hunter2\033[36m@127.0.0.1:3923/\033[0m
|
671
|
+
which should be the same as this header:
|
672
|
+
\033[36mAuthorization: Basic \033[35mZWQ6aHVudGVyMg==\033[0m
|
606
673
|
"""
|
607
674
|
),
|
608
675
|
],
|
@@ -754,6 +821,36 @@ def get_sects():
|
|
754
821
|
the upload speed can easily drop to 10% for small files)"""
|
755
822
|
),
|
756
823
|
],
|
824
|
+
[
|
825
|
+
"idp",
|
826
|
+
"replacing the login system with fancy middleware",
|
827
|
+
dedent(
|
828
|
+
"""
|
829
|
+
if you already have a centralized service which handles
|
830
|
+
user-authentication for other services already, you can
|
831
|
+
integrate copyparty with that for automatic login
|
832
|
+
|
833
|
+
if the middleware is providing the username in an http-header
|
834
|
+
named '\033[35mtheUsername\033[0m' then do this: \033[36m--idp-h-usr theUsername\033[0m
|
835
|
+
|
836
|
+
if the middleware is providing a list of groups in the header
|
837
|
+
named '\033[35mtheGroups\033[0m' then do this: \033[36m--idp-h-grp theGroup\033[0m
|
838
|
+
|
839
|
+
if the list of groups is separated by '\033[35m%\033[0m' then \033[36m--idp-gsep %\033[0m
|
840
|
+
|
841
|
+
if the middleware is providing a header named '\033[35mAccount\033[0m'
|
842
|
+
and the value is '\033[35malice@forest.net\033[0m' but the username is
|
843
|
+
actually '\033[35mmarisa\033[0m' then do this for each user:
|
844
|
+
\033[36m--idp-hm-usr ^Account^alice@forest.net^marisa\033[0m
|
845
|
+
(the separator '\033[35m^\033[0m' can be any character)
|
846
|
+
|
847
|
+
make ABSOLUTELY SURE that the header can only be set by your
|
848
|
+
middleware and not by clients! and, as an extra precaution,
|
849
|
+
send a header named '\033[36mfinalmasterspark\033[0m' (a secret keyword)
|
850
|
+
and then \033[36m--idp-h-key finalmasterspark\033[0m to require that
|
851
|
+
"""
|
852
|
+
),
|
853
|
+
],
|
757
854
|
[
|
758
855
|
"urlform",
|
759
856
|
"how to handle url-form POSTs",
|
@@ -1011,14 +1108,15 @@ def add_general(ap, nc, srvname):
|
|
1011
1108
|
|
1012
1109
|
def add_qr(ap, tty):
|
1013
1110
|
ap2 = ap.add_argument_group("qr options")
|
1014
|
-
ap2.add_argument("--qr", action="store_true", help="show
|
1015
|
-
ap2.add_argument("--qrs", action="store_true", help="
|
1111
|
+
ap2.add_argument("--qr", action="store_true", help="show QR-code on startup")
|
1112
|
+
ap2.add_argument("--qrs", action="store_true", help="change the QR-code URL to https://")
|
1016
1113
|
ap2.add_argument("--qrl", metavar="PATH", type=u, default="", help="location to include in the url, for example [\033[32mpriv/?pw=hunter2\033[0m]")
|
1017
1114
|
ap2.add_argument("--qri", metavar="PREFIX", type=u, default="", help="select IP which starts with \033[33mPREFIX\033[0m; [\033[32m.\033[0m] to force default IP when mDNS URL would have been used instead")
|
1018
|
-
ap2.add_argument("--qr-fg", metavar="COLOR", type=int, default=0 if tty else 16, help="foreground; try [\033[32m0\033[0m] if the qr-code is unreadable")
|
1115
|
+
ap2.add_argument("--qr-fg", metavar="COLOR", type=int, default=0 if tty else 16, help="foreground; try [\033[32m0\033[0m] or [\033[32m-1\033[0m] if the qr-code is unreadable")
|
1019
1116
|
ap2.add_argument("--qr-bg", metavar="COLOR", type=int, default=229, help="background (white=255)")
|
1020
1117
|
ap2.add_argument("--qrp", metavar="CELLS", type=int, default=4, help="padding (spec says 4 or more, but 1 is usually fine)")
|
1021
1118
|
ap2.add_argument("--qrz", metavar="N", type=int, default=0, help="[\033[32m1\033[0m]=1x, [\033[32m2\033[0m]=2x, [\033[32m0\033[0m]=auto (try [\033[32m2\033[0m] on broken fonts)")
|
1119
|
+
ap2.add_argument("--qr-pin", metavar="N", type=int, default=0, help="sticky/pin the qr-code to always stay on-screen; [\033[32m0\033[0m]=disabled, [\033[32m1\033[0m]=with-url, [\033[32m2\033[0m]=just-qr")
|
1022
1120
|
|
1023
1121
|
|
1024
1122
|
def add_fs(ap):
|
@@ -1048,6 +1146,7 @@ def add_upload(ap):
|
|
1048
1146
|
ap2.add_argument("--put-ck", metavar="ALG", type=u, default="sha512", help="default checksum-hasher for PUT/WebDAV uploads: no / md5 / sha1 / sha256 / sha512 / b2 / blake2 / b2s / blake2s (volflag=put_ck)")
|
1049
1147
|
ap2.add_argument("--bup-ck", metavar="ALG", type=u, default="sha512", help="default checksum-hasher for bup/basic-uploader: no / md5 / sha1 / sha256 / sha512 / b2 / blake2 / b2s / blake2s (volflag=bup_ck)")
|
1050
1148
|
ap2.add_argument("--unpost", metavar="SEC", type=int, default=3600*12, help="grace period where uploads can be deleted by the uploader, even without delete permissions; 0=disabled, default=12h")
|
1149
|
+
ap2.add_argument("--unp-who", metavar="NUM", type=int, default=1, help="clients can undo recent uploads by using the unpost tab (requires \033[33m-e2d\033[0m). [\033[32m0\033[0m] = never allowed (disable feature), [\033[32m1\033[0m] = allow if client has the same IP as the upload AND is using the same account, [\033[32m2\033[0m] = just check the IP, [\033[32m3\033[0m] = just check account-name (volflag=unp_who)")
|
1051
1150
|
ap2.add_argument("--u2abort", metavar="NUM", type=int, default=1, help="clients can abort incomplete uploads by using the unpost tab (requires \033[33m-e2d\033[0m). [\033[32m0\033[0m] = never allowed (disable feature), [\033[32m1\033[0m] = allow if client has the same IP as the upload AND is using the same account, [\033[32m2\033[0m] = just check the IP, [\033[32m3\033[0m] = just check account-name (volflag=u2abort)")
|
1052
1151
|
ap2.add_argument("--blank-wt", metavar="SEC", type=int, default=300, help="file write grace period (any client can write to a blank file last-modified more recently than \033[33mSEC\033[0m seconds ago)")
|
1053
1152
|
ap2.add_argument("--reg-cap", metavar="N", type=int, default=38400, help="max number of uploads to keep in memory when running without \033[33m-e2d\033[0m; roughly 1 MiB RAM per 600")
|
@@ -1141,7 +1240,8 @@ def add_auth(ap):
|
|
1141
1240
|
idp_db = os.path.join(E.cfg, "idp.db")
|
1142
1241
|
ses_db = os.path.join(E.cfg, "sessions.db")
|
1143
1242
|
ap2 = ap.add_argument_group("IdP / identity provider / user authentication options")
|
1144
|
-
ap2.add_argument("--idp-h-usr", metavar="HN", type=u,
|
1243
|
+
ap2.add_argument("--idp-h-usr", metavar="HN", type=u, action="append", help="\033[34mREPEATABLE:\033[0m bypass the copyparty authentication checks if the request-header \033[33mHN\033[0m contains a username to associate the request with (for use with authentik/oauth/...)\n\033[1;31mWARNING:\033[0m if you enable this, make sure clients are unable to specify this header themselves; must be washed away and replaced by a reverse-proxy")
|
1244
|
+
ap2.add_argument("--idp-hm-usr", metavar="TXT", type=u, action="append", help="\033[34mREPEATABLE:\033[0m bypass the copyparty authentication checks if the request-header \033[33mHN\033[0m is provided, and its value exists in a mapping defined by this option; see --help-idp")
|
1145
1245
|
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")
|
1146
1246
|
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")
|
1147
1247
|
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")
|
@@ -1154,7 +1254,11 @@ def add_auth(ap):
|
|
1154
1254
|
ap2.add_argument("--ses-db", metavar="PATH", type=u, default=ses_db, help="where to store the sessions database (if you run multiple copyparty instances, make sure they use different DBs)")
|
1155
1255
|
ap2.add_argument("--ses-len", metavar="CHARS", type=int, default=20, help="session key length; default is 120 bits ((20//4)*4*6)")
|
1156
1256
|
ap2.add_argument("--no-ses", action="store_true", help="disable sessions; use plaintext passwords in cookies")
|
1257
|
+
ap2.add_argument("--grp-all", metavar="NAME", type=u, default="acct", help="the name of the auto-generated group which contains every username which is known")
|
1157
1258
|
ap2.add_argument("--ipu", metavar="CIDR=USR", type=u, action="append", help="\033[34mREPEATABLE:\033[0m users with IP matching \033[33mCIDR\033[0m are auto-authenticated as username \033[33mUSR\033[0m; example: [\033[32m172.16.24.0/24=dave]")
|
1259
|
+
ap2.add_argument("--ipr", metavar="CIDR=USR", type=u, action="append", help="\033[34mREPEATABLE:\033[0m username \033[33mUSR\033[0m can only connect from an IP matching one or more \033[33mCIDR\033[0m (comma-sep.); example: [\033[32m192.168.123.0/24,172.16.0.0/16=dave]")
|
1260
|
+
ap2.add_argument("--have-idp-hdrs", type=u, default="", help=argparse.SUPPRESS)
|
1261
|
+
ap2.add_argument("--have-ipu-or-ipr", type=u, default="", help=argparse.SUPPRESS)
|
1158
1262
|
|
1159
1263
|
|
1160
1264
|
def add_chpw(ap):
|
@@ -1193,6 +1297,7 @@ def add_zc_mdns(ap):
|
|
1193
1297
|
ap2.add_argument("--zm-lh", metavar="PATH", type=u, default="", help="link a specific folder for http shares")
|
1194
1298
|
ap2.add_argument("--zm-lf", metavar="PATH", type=u, default="", help="link a specific folder for ftp shares")
|
1195
1299
|
ap2.add_argument("--zm-ls", metavar="PATH", type=u, default="", help="link a specific folder for smb shares")
|
1300
|
+
ap2.add_argument("--zm-fqdn", metavar="FQDN", type=u, default="--name.local", help="the domain to announce; NOTE: using anything other than .local is nonstandard and could cause problems")
|
1196
1301
|
ap2.add_argument("--zm-mnic", action="store_true", help="merge NICs which share subnets; assume that same subnet means same network")
|
1197
1302
|
ap2.add_argument("--zm-msub", action="store_true", help="merge subnets on each NIC -- always enabled for ipv6 -- reduces network load, but gnome-gvfs clients may stop working, and clients cannot be in subnets that the server is not")
|
1198
1303
|
ap2.add_argument("--zm-noneg", action="store_true", help="disable NSEC replies -- try this if some clients don't see copyparty")
|
@@ -1309,6 +1414,7 @@ def add_optouts(ap):
|
|
1309
1414
|
ap2.add_argument("--no-del", action="store_true", help="disable delete operations")
|
1310
1415
|
ap2.add_argument("--no-mv", action="store_true", help="disable move/rename operations")
|
1311
1416
|
ap2.add_argument("--no-cp", action="store_true", help="disable copy operations")
|
1417
|
+
ap2.add_argument("--no-fs-abrt", action="store_true", help="disable ability to abort ongoing copy/move")
|
1312
1418
|
ap2.add_argument("-nth", action="store_true", help="no title hostname; don't show \033[33m--name\033[0m in <title>")
|
1313
1419
|
ap2.add_argument("-nih", action="store_true", help="no info hostname -- don't show in UI")
|
1314
1420
|
ap2.add_argument("-nid", action="store_true", help="no info disk-usage -- don't show in UI")
|
@@ -1330,7 +1436,7 @@ def add_optouts(ap):
|
|
1330
1436
|
def add_safety(ap):
|
1331
1437
|
ap2 = ap.add_argument_group("safety options")
|
1332
1438
|
ap2.add_argument("-s", action="count", default=0, help="increase safety: Disable thumbnails / potentially dangerous software (ffmpeg/pillow/vips), hide partial uploads, avoid crawlers.\n └─Alias of\033[32m --dotpart --no-thumb --no-mtag-ff --no-robots --force-js")
|
1333
|
-
ap2.add_argument("-ss", action="store_true", help="further increase safety: Prevent js-injection, accidental move/delete, broken symlinks, webdav, 404 on 403, ban on excessive 404s.\n └─Alias of\033[32m -s --unpost=0 --no-del --no-mv --hardlink --vague-403 -nih")
|
1439
|
+
ap2.add_argument("-ss", action="store_true", help="further increase safety: Prevent js-injection, accidental move/delete, broken symlinks, webdav requires login, 404 on 403, ban on excessive 404s.\n └─Alias of\033[32m -s --unpost=0 --no-del --no-mv --hardlink --dav-auth --vague-403 -nih")
|
1334
1440
|
ap2.add_argument("-sss", action="store_true", help="further increase safety: Enable logging to disk, scan for dangerous symlinks.\n └─Alias of\033[32m -ss --no-dav --no-logues --no-readme -lo=cpp-%%Y-%%m%%d-%%H%%M%%S.txt.xz --ls=**,*,ln,p,r")
|
1335
1441
|
ap2.add_argument("--ls", metavar="U[,V[,F]]", type=u, default="", help="do a sanity/safety check of all volumes on startup; arguments \033[33mUSER\033[0m,\033[33mVOL\033[0m,\033[33mFLAGS\033[0m (see \033[33m--help-ls\033[0m); example [\033[32m**,*,ln,p,r\033[0m]")
|
1336
1442
|
ap2.add_argument("--xvol", action="store_true", help="never follow symlinks leaving the volume root, unless the link is into another volume where the user has similar access (volflag=xvol)")
|
@@ -1352,6 +1458,8 @@ def add_safety(ap):
|
|
1352
1458
|
ap2.add_argument("--sus-urls", metavar="R", type=u, default=r"\.php$|(^|/)wp-(admin|content|includes)/", help="URLs which are considered sus / eligible for banning; disable with blank or [\033[32mno\033[0m]")
|
1353
1459
|
ap2.add_argument("--nonsus-urls", metavar="R", type=u, default=r"^(favicon\.ico|robots\.txt)$|^apple-touch-icon|^\.well-known", help="harmless URLs ignored from 404-bans; disable with blank or [\033[32mno\033[0m]")
|
1354
1460
|
ap2.add_argument("--early-ban", action="store_true", help="if a client is banned, reject its connection as soon as possible; not a good idea to enable when proxied behind cloudflare since it could ban your reverse-proxy")
|
1461
|
+
ap2.add_argument("--cookie-nmax", metavar="N", type=int, default=50, help="reject HTTP-request from client if they send more than N cookies")
|
1462
|
+
ap2.add_argument("--cookie-cmax", metavar="N", type=int, default=8192, help="reject HTTP-request from client if more than N characters in Cookie header")
|
1355
1463
|
ap2.add_argument("--aclose", metavar="MIN", type=int, default=10, help="if a client maxes out the server connection limit, downgrade it from connection:keep-alive to connection:close for \033[33mMIN\033[0m minutes (and also kill its active connections) -- disable with 0")
|
1356
1464
|
ap2.add_argument("--loris", metavar="B", type=int, default=60, help="if a client maxes out the server connection limit without sending headers, ban it for \033[33mB\033[0m minutes; disable with [\033[32m0\033[0m]")
|
1357
1465
|
ap2.add_argument("--acao", metavar="V[,V]", type=u, default="*", help="Access-Control-Allow-Origin; list of origins (domains/IPs without port) to accept requests from; [\033[32mhttps://1.2.3.4\033[0m]. Default [\033[32m*\033[0m] allows requests from all sites but removes cookies and http-auth; only ?pw=hunter2 survives")
|
@@ -1382,7 +1490,7 @@ def add_shutdown(ap):
|
|
1382
1490
|
def add_logging(ap):
|
1383
1491
|
ap2 = ap.add_argument_group("logging options")
|
1384
1492
|
ap2.add_argument("-q", action="store_true", help="quiet; disable most STDOUT messages")
|
1385
|
-
ap2.add_argument("-lo", metavar="PATH", type=u, default="", help="logfile
|
1493
|
+
ap2.add_argument("-lo", metavar="PATH", type=u, default="", help="logfile; use .txt for plaintext or .xz for compressed. Example: \033[32mcpp-%%Y-%%m%%d-%%H%%M%%S.txt.xz\033[0m (NB: some errors may appear on STDOUT only)")
|
1386
1494
|
ap2.add_argument("--no-ansi", action="store_true", default=not VT100, help="disable colors; same as environment-variable NO_COLOR")
|
1387
1495
|
ap2.add_argument("--ansi", action="store_true", help="force colors; overrides environment-variable NO_COLOR")
|
1388
1496
|
ap2.add_argument("--no-logflush", action="store_true", help="don't flush the logfile after each write; tiny bit faster")
|
@@ -1418,11 +1526,12 @@ def add_thumbnail(ap):
|
|
1418
1526
|
ap2.add_argument("--no-athumb", action="store_true", help="disable audio thumbnails (spectrograms) (volflag=dathumb)")
|
1419
1527
|
ap2.add_argument("--th-size", metavar="WxH", default="320x256", help="thumbnail res (volflag=thsize)")
|
1420
1528
|
ap2.add_argument("--th-mt", metavar="CORES", type=int, default=CORES, help="num cpu cores to use for generating thumbnails")
|
1421
|
-
ap2.add_argument("--th-convt", metavar="SEC", type=float, default=60.0, help="
|
1529
|
+
ap2.add_argument("--th-convt", metavar="SEC", type=float, default=60.0, help="convert-to-image timeout in seconds (volflag=convt)")
|
1530
|
+
ap2.add_argument("--ac-convt", metavar="SEC", type=float, default=150.0, help="convert-to-audio timeout in seconds (volflag=aconvt)")
|
1422
1531
|
ap2.add_argument("--th-ram-max", metavar="GB", type=float, default=th_ram, help="max memory usage (GiB) permitted by thumbnailer; not very accurate")
|
1423
1532
|
ap2.add_argument("--th-crop", metavar="TXT", type=u, default="y", help="crop thumbnails to 4:3 or keep dynamic height; client can override in UI unless force. [\033[32my\033[0m]=crop, [\033[32mn\033[0m]=nocrop, [\033[32mfy\033[0m]=force-y, [\033[32mfn\033[0m]=force-n (volflag=crop)")
|
1424
1533
|
ap2.add_argument("--th-x3", metavar="TXT", type=u, default="n", help="show thumbs at 3x resolution; client can override in UI unless force. [\033[32my\033[0m]=yes, [\033[32mn\033[0m]=no, [\033[32mfy\033[0m]=force-yes, [\033[32mfn\033[0m]=force-no (volflag=th3x)")
|
1425
|
-
ap2.add_argument("--th-dec", metavar="LIBS", default="vips,pil,ff", help="image decoders, in order of preference")
|
1534
|
+
ap2.add_argument("--th-dec", metavar="LIBS", default="vips,pil,raw,ff", help="image decoders, in order of preference")
|
1426
1535
|
ap2.add_argument("--th-no-jpg", action="store_true", help="disable jpg output")
|
1427
1536
|
ap2.add_argument("--th-no-webp", action="store_true", help="disable webp output")
|
1428
1537
|
ap2.add_argument("--th-ff-jpg", action="store_true", help="force jpg output for video thumbs (avoids issues on some FFmpeg builds)")
|
@@ -1431,16 +1540,19 @@ def add_thumbnail(ap):
|
|
1431
1540
|
ap2.add_argument("--th-clean", metavar="SEC", type=int, default=43200, help="cleanup interval; 0=disabled")
|
1432
1541
|
ap2.add_argument("--th-maxage", metavar="SEC", type=int, default=604800, help="max folder age -- folders which haven't been poked for longer than \033[33m--th-poke\033[0m seconds will get deleted every \033[33m--th-clean\033[0m seconds")
|
1433
1542
|
ap2.add_argument("--th-covers", metavar="N,N", type=u, default="folder.png,folder.jpg,cover.png,cover.jpg", help="folder thumbnails to stat/look for; enabling \033[33m-e2d\033[0m will make these case-insensitive, and try them as dotfiles (.folder.jpg), and also automatically select thumbnails for all folders that contain pics, even if none match this pattern")
|
1543
|
+
ap2.add_argument("--th-spec-p", metavar="N", type=u, default=1, help="for music, do spectrograms or embedded coverart? [\033[32m0\033[0m]=only-art, [\033[32m1\033[0m]=prefer-art, [\033[32m2\033[0m]=only-spec")
|
1434
1544
|
# https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html
|
1435
1545
|
# https://github.com/libvips/libvips
|
1546
|
+
# https://stackoverflow.com/a/47612661
|
1436
1547
|
# ffmpeg -hide_banner -demuxers | awk '/^ D /{print$2}' | while IFS= read -r x; do ffmpeg -hide_banner -h demuxer=$x; done | grep -E '^Demuxer |extensions:'
|
1437
|
-
ap2.add_argument("--th-r-pil", metavar="T,T", type=u, default="avif,avifs,blp,bmp,cbz,dcx,dds,dib,emf,eps,fits,flc,fli,fpx,gif,heic,heics,heif,heifs,icns,ico,im,j2p,j2k,jp2,jpeg,jpg,jpx,pbm,pcx,pgm,png,pnm,ppm,psd,qoi,sgi,spi,tga,tif,tiff,webp,wmf,xbm,xpm", help="image formats to decode using pillow")
|
1438
|
-
ap2.add_argument("--th-r-vips", metavar="T,T", type=u, default="avif,exr,fit,fits,fts,gif,hdr,heic,jp2,jpeg,jpg,jpx,jxl,nii,pfm,pgm,png,ppm,svg,tif,tiff,webp", help="image formats to decode using pyvips")
|
1439
|
-
ap2.add_argument("--th-r-
|
1548
|
+
ap2.add_argument("--th-r-pil", metavar="T,T", type=u, default="avif,avifs,blp,bmp,cbz,dcx,dds,dib,emf,eps,epub,fits,flc,fli,fpx,gif,heic,heics,heif,heifs,icns,ico,im,j2p,j2k,jp2,jpeg,jpg,jpx,pbm,pcx,pgm,png,pnm,ppm,psd,qoi,sgi,spi,tga,tif,tiff,webp,wmf,xbm,xpm", help="image formats to decode using pillow")
|
1549
|
+
ap2.add_argument("--th-r-vips", metavar="T,T", type=u, default="avif,exr,fit,fits,fts,gif,hdr,heic,heics,heif,heifs,jp2,jpeg,jpg,jpx,jxl,nii,pfm,pgm,png,ppm,svg,tif,tiff,webp", help="image formats to decode using pyvips")
|
1550
|
+
ap2.add_argument("--th-r-raw", metavar="T,T", type=u, default="arw,cr2,cr3,crw,dcr,dng,erf,k25,kdc,mrw,nef,orf,pef,raf,raw,sr2,srf,x3f", help="image formats to decode using rawpy")
|
1551
|
+
ap2.add_argument("--th-r-ffi", metavar="T,T", type=u, default="apng,avif,avifs,bmp,cbz,dds,dib,epub,fit,fits,fts,gif,hdr,heic,heics,heif,heifs,icns,ico,jp2,jpeg,jpg,jpx,jxl,pbm,pcx,pfm,pgm,png,pnm,ppm,psd,qoi,sgi,tga,tif,tiff,webp,xbm,xpm", help="image formats to decode using ffmpeg")
|
1440
1552
|
ap2.add_argument("--th-r-ffv", metavar="T,T", type=u, default="3gp,asf,av1,avc,avi,flv,h264,h265,hevc,m4v,mjpeg,mjpg,mkv,mov,mp4,mpeg,mpeg2,mpegts,mpg,mpg2,mts,nut,ogm,ogv,rm,ts,vob,webm,wmv", help="video formats to decode using ffmpeg")
|
1441
1553
|
ap2.add_argument("--th-r-ffa", metavar="T,T", type=u, default="aac,ac3,aif,aiff,alac,alaw,amr,apac,ape,au,bonk,dfpwm,dts,flac,gsm,ilbc,it,itgz,itxz,itz,m4a,mdgz,mdxz,mdz,mo3,mod,mp2,mp3,mpc,mptm,mt2,mulaw,oga,ogg,okt,opus,ra,s3m,s3gz,s3xz,s3z,tak,tta,ulaw,wav,wma,wv,xm,xmgz,xmxz,xmz,xpk", help="audio formats to decode using ffmpeg")
|
1442
1554
|
ap2.add_argument("--th-spec-cnv", metavar="T", type=u, default="it,itgz,itxz,itz,mdgz,mdxz,mdz,mo3,mod,s3m,s3gz,s3xz,s3z,xm,xmgz,xmxz,xmz,xpk", help="audio formats which provoke https://trac.ffmpeg.org/ticket/10797 (huge ram usage for s3xmodit spectrograms)")
|
1443
|
-
ap2.add_argument("--au-unpk", metavar="E=F.C", type=u, default="mdz=mod.zip, mdgz=mod.gz, mdxz=mod.xz, s3z=s3m.zip, s3gz=s3m.gz, s3xz=s3m.xz, xmz=xm.zip, xmgz=xm.gz, xmxz=xm.xz, itz=it.zip, itgz=it.gz, itxz=it.xz, cbz=jpg.cbz", help="audio/image formats to decompress before passing to ffmpeg")
|
1555
|
+
ap2.add_argument("--au-unpk", metavar="E=F.C", type=u, default="mdz=mod.zip, mdgz=mod.gz, mdxz=mod.xz, s3z=s3m.zip, s3gz=s3m.gz, s3xz=s3m.xz, xmz=xm.zip, xmgz=xm.gz, xmxz=xm.xz, itz=it.zip, itgz=it.gz, itxz=it.xz, cbz=jpg.cbz, epub=jpg.epub", help="audio/image formats to decompress before passing to ffmpeg")
|
1444
1556
|
|
1445
1557
|
|
1446
1558
|
def add_transcoding(ap):
|
@@ -1485,8 +1597,8 @@ def add_db_general(ap, hcores):
|
|
1485
1597
|
ap2.add_argument("-e2vp", action="store_true", help="on hash mismatch: panic and quit copyparty")
|
1486
1598
|
ap2.add_argument("--hist", metavar="PATH", type=u, default="", help="where to store volume data (db, thumbs); default is a folder named \".hist\" inside each volume (volflag=hist)")
|
1487
1599
|
ap2.add_argument("--dbpath", metavar="PATH", type=u, default="", help="override where the volume databases are to be placed; default is the same as \033[33m--hist\033[0m (volflag=dbpath)")
|
1488
|
-
ap2.add_argument("--no-hash", metavar="PTN", type=u, default="", help="regex: disable hashing of matching absolute-filesystem-paths during e2ds folder scans (volflag=nohash)")
|
1489
|
-
ap2.add_argument("--no-idx", metavar="PTN", type=u, default=noidx, help="regex: disable indexing of matching absolute-filesystem-paths during e2ds folder
|
1600
|
+
ap2.add_argument("--no-hash", metavar="PTN", type=u, default="", help="regex: disable hashing of matching absolute-filesystem-paths during e2ds folder scans (must be specified as one big regex, not multiple times) (volflag=nohash)")
|
1601
|
+
ap2.add_argument("--no-idx", metavar="PTN", type=u, default=noidx, help="regex: disable indexing of matching absolute-filesystem-paths during e2ds folder scan (must be specified as one big regex, not multiple times) (volflag=noidx)")
|
1490
1602
|
ap2.add_argument("--no-dirsz", action="store_true", help="do not show total recursive size of folders in listings, show inode size instead; slightly faster (volflag=nodirsz)")
|
1491
1603
|
ap2.add_argument("--re-dirsz", action="store_true", help="if the directory-sizes in the UI are bonkers, use this along with \033[33m-e2dsa\033[0m to rebuild the index from scratch")
|
1492
1604
|
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")
|
@@ -1552,13 +1664,14 @@ def add_og(ap):
|
|
1552
1664
|
|
1553
1665
|
|
1554
1666
|
def add_ui(ap, retry):
|
1667
|
+
THEMES = 10
|
1555
1668
|
ap2 = ap.add_argument_group("ui options")
|
1556
1669
|
ap2.add_argument("--grid", action="store_true", help="show grid/thumbnails by default (volflag=grid)")
|
1557
1670
|
ap2.add_argument("--gsel", action="store_true", help="select files in grid by ctrl-click (volflag=gsel)")
|
1558
1671
|
ap2.add_argument("--localtime", action="store_true", help="default to local timezone instead of UTC")
|
1559
|
-
ap2.add_argument("--lang", metavar="LANG", type=u, default="eng", help="language
|
1560
|
-
ap2.add_argument("--theme", metavar="NUM", type=int, default=0, help="default theme to use (0
|
1561
|
-
ap2.add_argument("--themes", metavar="NUM", type=int, default=
|
1672
|
+
ap2.add_argument("--lang", metavar="LANG", type=u, default="eng", help="language, for example \033[32meng\033[0m / \033[32mnor\033[0m / ...")
|
1673
|
+
ap2.add_argument("--theme", metavar="NUM", type=int, default=0, help="default theme to use (0..%d)" % (THEMES - 1,))
|
1674
|
+
ap2.add_argument("--themes", metavar="NUM", type=int, default=THEMES, help="number of themes installed")
|
1562
1675
|
ap2.add_argument("--au-vol", metavar="0-100", type=int, default=50, choices=range(0, 101), help="default audio/video volume percent")
|
1563
1676
|
ap2.add_argument("--sort", metavar="C,C,C", type=u, default="href", help="default sort order, comma-separated column IDs (see header tooltips), prefix with '-' for descending. Examples: \033[32mhref -href ext sz ts tags/Album tags/.tn\033[0m (volflag=sort)")
|
1564
1677
|
ap2.add_argument("--nsort", action="store_true", help="default-enable natural sort of filenames with leading numbers (volflag=nsort)")
|
@@ -1759,16 +1872,7 @@ def main(argv = None) :
|
|
1759
1872
|
|
1760
1873
|
ensure_webdeps()
|
1761
1874
|
|
1762
|
-
|
1763
|
-
if k == "-c" and os.path.isfile(v):
|
1764
|
-
supp = args_from_cfg(v)
|
1765
|
-
argv.extend(supp)
|
1766
|
-
|
1767
|
-
for k in argv[1:]:
|
1768
|
-
v = k[2:]
|
1769
|
-
if k.startswith("-c") and v and os.path.isfile(v):
|
1770
|
-
supp = args_from_cfg(v)
|
1771
|
-
argv.extend(supp)
|
1875
|
+
argv = expand_cfg(argv)
|
1772
1876
|
|
1773
1877
|
deprecated = [
|
1774
1878
|
("--salt", "--warksalt"),
|
copyparty/__version__.py
CHANGED
copyparty/authsrv.py
CHANGED
@@ -874,6 +874,15 @@ class VFS(object):
|
|
874
874
|
return None
|
875
875
|
|
876
876
|
if "xvol" in self.flags:
|
877
|
+
self_ap = self.realpath + os.sep
|
878
|
+
if aps.startswith(self_ap):
|
879
|
+
vp = aps[len(self_ap) :]
|
880
|
+
if ANYWIN:
|
881
|
+
vp = vp.replace(os.sep, "/")
|
882
|
+
vn2, _ = self._find(vp)
|
883
|
+
if self == vn2:
|
884
|
+
return self
|
885
|
+
|
877
886
|
all_aps = self.shr_all_aps or self.root.all_aps
|
878
887
|
|
879
888
|
for vap, vns in all_aps:
|
@@ -1091,6 +1100,9 @@ class AuthSrv(object):
|
|
1091
1100
|
if rejected:
|
1092
1101
|
continue
|
1093
1102
|
|
1103
|
+
if gn == self.args.grp_all:
|
1104
|
+
gn = ""
|
1105
|
+
|
1094
1106
|
# if ap/vp has a user/group placeholder, make sure to keep
|
1095
1107
|
# track so the same user/group is mapped when setting perms;
|
1096
1108
|
# otherwise clear un/gn to indicate it's a regular volume
|
@@ -1200,6 +1212,7 @@ class AuthSrv(object):
|
|
1200
1212
|
self.load_idp_db(bool(self.idp_accs))
|
1201
1213
|
ret = {un: gns[:] for un, gns in self.idp_accs.items()}
|
1202
1214
|
ret.update({zs: [""] for zs in acct if zs not in ret})
|
1215
|
+
grps[self.args.grp_all] = list(ret.keys())
|
1203
1216
|
for gn, uns in grps.items():
|
1204
1217
|
for un in uns:
|
1205
1218
|
try:
|
@@ -1677,6 +1690,9 @@ class AuthSrv(object):
|
|
1677
1690
|
self.log("\n{0}\n{1}{0}".format(t, "\n".join(slns)))
|
1678
1691
|
raise
|
1679
1692
|
|
1693
|
+
self.args.have_idp_hdrs = bool(self.args.idp_h_usr or self.args.idp_hm_usr)
|
1694
|
+
self.args.have_ipu_or_ipr = bool(self.args.ipu or self.args.ipr)
|
1695
|
+
|
1680
1696
|
self.setup_pwhash(acct)
|
1681
1697
|
defpw = acct.copy()
|
1682
1698
|
self.setup_chpw(acct)
|
@@ -1689,7 +1705,7 @@ class AuthSrv(object):
|
|
1689
1705
|
|
1690
1706
|
mount = cased
|
1691
1707
|
|
1692
|
-
if not mount and not self.args.
|
1708
|
+
if not mount and not self.args.have_idp_hdrs:
|
1693
1709
|
# -h says our defaults are CWD at root and read/write for everyone
|
1694
1710
|
axs = AXS(["*"], ["*"], None, None)
|
1695
1711
|
ehint = ""
|
@@ -1861,7 +1877,7 @@ class AuthSrv(object):
|
|
1861
1877
|
|
1862
1878
|
if missing_users:
|
1863
1879
|
zs = ", ".join(k for k in sorted(missing_users))
|
1864
|
-
if self.args.
|
1880
|
+
if self.args.have_idp_hdrs:
|
1865
1881
|
t = "the following users are unknown, and assumed to come from IdP: "
|
1866
1882
|
self.log(t + zs, c=6)
|
1867
1883
|
else:
|
@@ -1872,6 +1888,16 @@ class AuthSrv(object):
|
|
1872
1888
|
if LEELOO_DALLAS in all_users:
|
1873
1889
|
raise Exception("sorry, reserved username: " + LEELOO_DALLAS)
|
1874
1890
|
|
1891
|
+
zsl = []
|
1892
|
+
for usr in list(acct)[:]:
|
1893
|
+
zs = acct[usr].strip()
|
1894
|
+
if not zs:
|
1895
|
+
zs = ub64enc(os.urandom(48)).decode("ascii")
|
1896
|
+
zsl.append(usr)
|
1897
|
+
acct[usr] = zs
|
1898
|
+
if zsl:
|
1899
|
+
self.log("generated random passwords for users %r" % (zsl,), 6)
|
1900
|
+
|
1875
1901
|
seenpwds = {}
|
1876
1902
|
for usr, pwd in acct.items():
|
1877
1903
|
if pwd in seenpwds:
|
@@ -2192,12 +2218,12 @@ class AuthSrv(object):
|
|
2192
2218
|
if vf not in vol.flags:
|
2193
2219
|
vol.flags[vf] = getattr(self.args, ga)
|
2194
2220
|
|
2195
|
-
zs = "forget_ip gid nrand tail_who u2abort u2ow uid ups_who zip_who"
|
2221
|
+
zs = "forget_ip gid nrand tail_who th_spec_p u2abort u2ow uid unp_who ups_who zip_who"
|
2196
2222
|
for k in zs.split():
|
2197
2223
|
if k in vol.flags:
|
2198
2224
|
vol.flags[k] = int(vol.flags[k])
|
2199
2225
|
|
2200
|
-
zs = "convt tail_fd tail_rate tail_tmax"
|
2226
|
+
zs = "aconvt convt tail_fd tail_rate tail_tmax"
|
2201
2227
|
for k in zs.split():
|
2202
2228
|
if k in vol.flags:
|
2203
2229
|
vol.flags[k] = float(vol.flags[k])
|
@@ -2528,7 +2554,7 @@ class AuthSrv(object):
|
|
2528
2554
|
if not self.args.no_voldump:
|
2529
2555
|
self.log(t)
|
2530
2556
|
|
2531
|
-
if have_e2d or self.args.
|
2557
|
+
if have_e2d or self.args.have_idp_hdrs:
|
2532
2558
|
t = self.chk_sqlite_threadsafe()
|
2533
2559
|
if t:
|
2534
2560
|
self.log("\n\033[{}\033[0m\n".format(t))
|
@@ -2817,7 +2843,7 @@ class AuthSrv(object):
|
|
2817
2843
|
def load_idp_db(self, quiet=False) :
|
2818
2844
|
# mutex me
|
2819
2845
|
level = self.args.idp_store
|
2820
|
-
if level < 2 or not self.args.
|
2846
|
+
if level < 2 or not self.args.have_idp_hdrs:
|
2821
2847
|
return
|
2822
2848
|
|
2823
2849
|
|
@@ -2872,7 +2898,7 @@ class AuthSrv(object):
|
|
2872
2898
|
n = []
|
2873
2899
|
q = "insert into us values (?,?,?)"
|
2874
2900
|
accs = list(self.acct)
|
2875
|
-
if self.args.
|
2901
|
+
if self.args.have_idp_hdrs and self.args.idp_cookie:
|
2876
2902
|
accs.extend(self.idp_accs.keys())
|
2877
2903
|
for uname in accs:
|
2878
2904
|
if uname not in ases:
|
copyparty/cfg.py
CHANGED
@@ -68,6 +68,7 @@ def vf_bmap() :
|
|
68
68
|
def vf_vmap() :
|
69
69
|
"""argv-to-volflag: simple values"""
|
70
70
|
ret = {
|
71
|
+
"ac_convt": "aconvt",
|
71
72
|
"no_hash": "nohash",
|
72
73
|
"no_idx": "noidx",
|
73
74
|
"re_maxage": "scan",
|
@@ -111,12 +112,14 @@ def vf_vmap() :
|
|
111
112
|
"tail_tmax",
|
112
113
|
"tail_who",
|
113
114
|
"tcolor",
|
115
|
+
"th_spec_p",
|
114
116
|
"txt_eol",
|
115
117
|
"unlist",
|
116
118
|
"u2abort",
|
117
119
|
"u2ts",
|
118
120
|
"uid",
|
119
121
|
"gid",
|
122
|
+
"unp_who",
|
120
123
|
"ups_who",
|
121
124
|
"zip_who",
|
122
125
|
"zipmaxn",
|
@@ -260,7 +263,9 @@ flagcats = {
|
|
260
263
|
"thsize": "thumbnail res; WxH",
|
261
264
|
"crop": "center-cropping (y/n/fy/fn)",
|
262
265
|
"th3x": "3x resolution (y/n/fy/fn)",
|
263
|
-
"convt": "
|
266
|
+
"convt": "convert-to-image timeout in seconds",
|
267
|
+
"aconvt": "convert-to-audio timeout in seconds",
|
268
|
+
"th_spec_p=1": "make spectrograms? 0=never 1=fallback 2=always",
|
264
269
|
"ext_th=s=/b.png": "use /b.png as thumbnail for file-extension s",
|
265
270
|
},
|
266
271
|
"handlers\n(better explained in --help-handlers)": {
|
@@ -341,6 +346,7 @@ flagcats = {
|
|
341
346
|
"dky": 'allow seeing files (not folders) inside a specific folder\nwith "g" perm, and does not require a valid dirkey to do so',
|
342
347
|
"rss": "allow '?rss' URL suffix (experimental)",
|
343
348
|
"rmagic": "expensive analysis for mimetype accuracy",
|
349
|
+
"unp_who=2": "unpost only if same... 1=ip+name, 2=ip, 3=name",
|
344
350
|
"ups_who=2": "restrict viewing the list of recent uploads",
|
345
351
|
"zip_who=2": "restrict access to download-as-zip/tar",
|
346
352
|
"zipmaxn=9k": "reject download-as-zip if more than 9000 files",
|
copyparty/dxml.py
CHANGED
copyparty/ftpd.py
CHANGED
@@ -92,6 +92,10 @@ class FtpAuth(DummyAuthorizer):
|
|
92
92
|
|
93
93
|
if args.ipu and uname == "*":
|
94
94
|
uname = args.ipu_iu[args.ipu_nm.map(ip)]
|
95
|
+
if args.ipr and uname in args.ipr_u:
|
96
|
+
if not args.ipr_u[uname].map(ip):
|
97
|
+
logging.warning("username [%s] rejected by --ipr", uname)
|
98
|
+
uname = "*"
|
95
99
|
|
96
100
|
if not uname or not (asrv.vfs.aread.get(uname) or asrv.vfs.awrite.get(uname)):
|
97
101
|
g = self.hub.gpwd
|
@@ -281,9 +285,12 @@ class FtpFs(AbstractedFS):
|
|
281
285
|
# returning 550 is library-default and suitable
|
282
286
|
raise FSE("No such file or directory")
|
283
287
|
|
284
|
-
|
285
|
-
|
286
|
-
|
288
|
+
if vfs.realpath:
|
289
|
+
avfs = vfs.chk_ap(ap, st)
|
290
|
+
if not avfs:
|
291
|
+
raise FSE("Permission denied", 1)
|
292
|
+
else:
|
293
|
+
avfs = vfs
|
287
294
|
|
288
295
|
self.cwd = nwd
|
289
296
|
(
|
@@ -398,8 +405,12 @@ class FtpFs(AbstractedFS):
|
|
398
405
|
return st
|
399
406
|
|
400
407
|
def utime(self, path , timeval ) :
|
401
|
-
|
402
|
-
|
408
|
+
try:
|
409
|
+
ap = self.rv2a(path, w=True)[0]
|
410
|
+
return bos.utime(ap, (int(time.time()), int(timeval)))
|
411
|
+
except Exception as ex:
|
412
|
+
logging.error("ftp.utime: %s, %r", ex, ex)
|
413
|
+
raise
|
403
414
|
|
404
415
|
def lstat(self, path ) :
|
405
416
|
ap = self.rv2a(path)[0]
|
@@ -488,7 +499,11 @@ class FtpHandler(FTPHandler):
|
|
488
499
|
def ftp_STOR(self, file , mode = "w") :
|
489
500
|
# Optional[str]
|
490
501
|
vp = join(self.fs.cwd, file).lstrip("/")
|
491
|
-
|
502
|
+
try:
|
503
|
+
ap, vfs, rem = self.fs.v2a(vp, w=True)
|
504
|
+
except Exception as ex:
|
505
|
+
self.respond("550 %s" % (ex,), logging.info)
|
506
|
+
return
|
492
507
|
self.vfs_map[ap] = vp
|
493
508
|
xbu = vfs.flags.get("xbu")
|
494
509
|
if xbu and not runhook(
|