Compare commits
368 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
012bbcf770 | ||
|
|
b54cb47b2e | ||
|
|
1b15f43745 | ||
|
|
96771bf1bd | ||
|
|
580078bddb | ||
|
|
c5c7080ec6 | ||
|
|
408339b51d | ||
|
|
02e3d44998 | ||
|
|
156f13ded1 | ||
|
|
d288467cb7 | ||
|
|
21662c9f3f | ||
|
|
9149fe6cdd | ||
|
|
9a146192b7 | ||
|
|
3a9d3b7b61 | ||
|
|
f03f0973ab | ||
|
|
7ec0881e8c | ||
|
|
59e1ab42ff | ||
|
|
722216b901 | ||
|
|
bd8f3dc368 | ||
|
|
33cd94a141 | ||
|
|
053ac74734 | ||
|
|
cced99fafa | ||
|
|
a009ff53f7 | ||
|
|
ca16c4108d | ||
|
|
d1b6c67dc3 | ||
|
|
a61f8133d5 | ||
|
|
38d797a544 | ||
|
|
16c1877f50 | ||
|
|
da5f15a778 | ||
|
|
396c64ecf7 | ||
|
|
252c3a7985 | ||
|
|
a3ecbf0ae7 | ||
|
|
314327d8f2 | ||
|
|
bfacd06929 | ||
|
|
4f5e8f8cf5 | ||
|
|
1fbb4c09cc | ||
|
|
b332e1992b | ||
|
|
5955940b82 | ||
|
|
231a03bcfd | ||
|
|
bc85723657 | ||
|
|
be32b743c6 | ||
|
|
83c9843059 | ||
|
|
11cf43626d | ||
|
|
a6dc5e2ce3 | ||
|
|
38593a0394 | ||
|
|
95309afeea | ||
|
|
c2bf6fe2a3 | ||
|
|
99ac324fbd | ||
|
|
5562de330f | ||
|
|
95014236ac | ||
|
|
6aa7386138 | ||
|
|
3226a1f588 | ||
|
|
b4cf890cd8 | ||
|
|
ce09e323af | ||
|
|
941aedb177 | ||
|
|
87a0d502a3 | ||
|
|
cab7c1b0b8 | ||
|
|
d5892341b6 | ||
|
|
646557a43e | ||
|
|
ed8d34ab43 | ||
|
|
5e34463c77 | ||
|
|
1b14eb7959 | ||
|
|
ed48c2d0ed | ||
|
|
26fe84b660 | ||
|
|
5938230270 | ||
|
|
1a33a047fa | ||
|
|
43a8bcefb9 | ||
|
|
2e740e513f | ||
|
|
8a21a86b61 | ||
|
|
f600116205 | ||
|
|
1c03705de8 | ||
|
|
f7e461fac6 | ||
|
|
03ce6c97ff | ||
|
|
ffd9e76e07 | ||
|
|
fc49cb1e67 | ||
|
|
f5712d9f25 | ||
|
|
161d57bdda | ||
|
|
bae0d440bf | ||
|
|
fff052dde1 | ||
|
|
73b06eaa02 | ||
|
|
08a8ebed17 | ||
|
|
74d07426b3 | ||
|
|
69a2bba99a | ||
|
|
4d685d78ee | ||
|
|
5845ec3f49 | ||
|
|
13373426fe | ||
|
|
8e55551a06 | ||
|
|
12a3f0ac31 | ||
|
|
18e33edc88 | ||
|
|
c72c5ad4ee | ||
|
|
0fbc81ab2f | ||
|
|
af0a34cf82 | ||
|
|
b4590c5398 | ||
|
|
f787a66230 | ||
|
|
b21a99fd62 | ||
|
|
eb16306cde | ||
|
|
7bc23687e3 | ||
|
|
e1eaa057f2 | ||
|
|
97c264ca3e | ||
|
|
cf848ab1f7 | ||
|
|
cf83f9b0fd | ||
|
|
d98e361083 | ||
|
|
ce7f5309c7 | ||
|
|
75c485ced7 | ||
|
|
9c6e2ec012 | ||
|
|
1a02948a61 | ||
|
|
8b05ba4ba1 | ||
|
|
21e2874cb7 | ||
|
|
360ed5c46c | ||
|
|
5099bc365d | ||
|
|
12986da147 | ||
|
|
23e72797bc | ||
|
|
ac7b6f8f55 | ||
|
|
981b9ff11e | ||
|
|
4186906f4c | ||
|
|
0850d24e0c | ||
|
|
7ab8334c96 | ||
|
|
a4d7329ab7 | ||
|
|
3f4eae6bce | ||
|
|
518cf4be57 | ||
|
|
71096182be | ||
|
|
6452e927ea | ||
|
|
bc70cfa6f0 | ||
|
|
2b6e5ebd2d | ||
|
|
c761bd799a | ||
|
|
2f7c2fdee4 | ||
|
|
70a76ec343 | ||
|
|
7c3f64abf2 | ||
|
|
f5f38f195c | ||
|
|
7e84f4f015 | ||
|
|
4802f8cf07 | ||
|
|
cc05e67d8f | ||
|
|
2b6b174517 | ||
|
|
a1d05e6e12 | ||
|
|
f95ceb6a9b | ||
|
|
8f91b0726d | ||
|
|
97807f4383 | ||
|
|
5f42237f2c | ||
|
|
68289cfa54 | ||
|
|
42ea30270f | ||
|
|
ebbbbf3d82 | ||
|
|
27516e2d16 | ||
|
|
84bb6f915e | ||
|
|
46752f758a | ||
|
|
34c4c22e61 | ||
|
|
af2d0b8421 | ||
|
|
638b05a49a | ||
|
|
7a13e8a7fc | ||
|
|
d9fa74711d | ||
|
|
41867f578f | ||
|
|
0bf41ed4ef | ||
|
|
d080b4a731 | ||
|
|
ca4232ada9 | ||
|
|
ad348f91c9 | ||
|
|
990f915f42 | ||
|
|
53d720217b | ||
|
|
7a06ff480d | ||
|
|
3ef551f788 | ||
|
|
f0125cdc36 | ||
|
|
ed5f6736df | ||
|
|
15d8be0fae | ||
|
|
46f3e61360 | ||
|
|
87ad8c98d4 | ||
|
|
9bbdc4100f | ||
|
|
c80307e8ff | ||
|
|
c1d77e1041 | ||
|
|
d9e83650dc | ||
|
|
f6d635acd9 | ||
|
|
0dbd8a01ff | ||
|
|
8d755d41e0 | ||
|
|
190473bd32 | ||
|
|
030d1ec254 | ||
|
|
5a2b91a084 | ||
|
|
a50a05e4e7 | ||
|
|
6cb5a87c79 | ||
|
|
b9f89ca552 | ||
|
|
26c9fd5dea | ||
|
|
e81a9b6fe0 | ||
|
|
452450e451 | ||
|
|
419dd2d1c7 | ||
|
|
ee86b06676 | ||
|
|
953183f16d | ||
|
|
228f71708b | ||
|
|
621471a7cb | ||
|
|
8b58e951e3 | ||
|
|
1db489a0aa | ||
|
|
be65c3c6cf | ||
|
|
46e7fa31fe | ||
|
|
66e21bd499 | ||
|
|
8cab4c01fd | ||
|
|
d52038366b | ||
|
|
4fcfd87f5b | ||
|
|
f893c6baa4 | ||
|
|
9a45549b66 | ||
|
|
ae3a01038b | ||
|
|
e47a2a4ca2 | ||
|
|
95ea6d5f78 | ||
|
|
7d290f6b8f | ||
|
|
9db617ed5a | ||
|
|
514456940a | ||
|
|
33feefd9cd | ||
|
|
65e14cf348 | ||
|
|
1d61bcc4f3 | ||
|
|
c38bbaca3c | ||
|
|
246d245ebc | ||
|
|
f269a710e2 | ||
|
|
051998429c | ||
|
|
432cdd640f | ||
|
|
9ed9b0964e | ||
|
|
6a97b3526d | ||
|
|
451d757996 | ||
|
|
f9e9eba3b1 | ||
|
|
2a9a6aebd9 | ||
|
|
adbb6c449e | ||
|
|
3993605324 | ||
|
|
0ae574ec2c | ||
|
|
c56ded828c | ||
|
|
02c7061945 | ||
|
|
9209e44cd3 | ||
|
|
ebed37394e | ||
|
|
4c7a2a7ec3 | ||
|
|
0a25a88a34 | ||
|
|
6aa9025347 | ||
|
|
a918cc67eb | ||
|
|
08f4695283 | ||
|
|
44e76d5eeb | ||
|
|
cfa36fd279 | ||
|
|
3d4166e006 | ||
|
|
07bac1c592 | ||
|
|
755f2ce1ba | ||
|
|
cca2844deb | ||
|
|
24a2f760b7 | ||
|
|
79bbd8fe38 | ||
|
|
35dce1e3e4 | ||
|
|
f886fdf913 | ||
|
|
4476f2f0da | ||
|
|
160f161700 | ||
|
|
c164fc58a2 | ||
|
|
0c625a4e62 | ||
|
|
bf3941cf7a | ||
|
|
3649e8288a | ||
|
|
9a45e26026 | ||
|
|
e65f127571 | ||
|
|
3bfc699787 | ||
|
|
955318428a | ||
|
|
f6279b356a | ||
|
|
4cc3cdc989 | ||
|
|
f9aa20a3ad | ||
|
|
129d33f1a0 | ||
|
|
1ad7a3f378 | ||
|
|
b533be8818 | ||
|
|
fb729e5166 | ||
|
|
d337ecdb20 | ||
|
|
5f1f0a48b0 | ||
|
|
e0f1cb94a5 | ||
|
|
a362ee2246 | ||
|
|
19f23c686e | ||
|
|
23b20ff4a6 | ||
|
|
72574da834 | ||
|
|
d5a79455d1 | ||
|
|
070d4b9da9 | ||
|
|
0ace22fffe | ||
|
|
9e483d7694 | ||
|
|
26458b7a06 | ||
|
|
b6a4604952 | ||
|
|
af752fbbc2 | ||
|
|
279c9d706a | ||
|
|
806e7b5530 | ||
|
|
f3dc6a217b | ||
|
|
7671d791fa | ||
|
|
8cd84608a5 | ||
|
|
980c6fc810 | ||
|
|
fb40a484c5 | ||
|
|
daa9dedcaa | ||
|
|
0d634345ac | ||
|
|
e648252479 | ||
|
|
179d7a9ad8 | ||
|
|
19bc962ad5 | ||
|
|
27cce086c6 | ||
|
|
fec0c620d4 | ||
|
|
05a1a31cab | ||
|
|
d020527c6f | ||
|
|
4451485664 | ||
|
|
a4e1a3738a | ||
|
|
4339dbeb8d | ||
|
|
5b0605774c | ||
|
|
e3684e25f8 | ||
|
|
1359213196 | ||
|
|
03efc6a169 | ||
|
|
15b5982211 | ||
|
|
0eb3a5d387 | ||
|
|
7f8777389c | ||
|
|
4eb20f10ad | ||
|
|
daa11df558 | ||
|
|
1bb0db30a0 | ||
|
|
02910b0020 | ||
|
|
23b8901c9c | ||
|
|
99f6ed0cd7 | ||
|
|
890c310880 | ||
|
|
0194eeb31f | ||
|
|
f9be4c62b1 | ||
|
|
027e8c18f1 | ||
|
|
4a3bb35a95 | ||
|
|
4bfb0d4494 | ||
|
|
7e0ef03a1e | ||
|
|
f7dbd95a54 | ||
|
|
515ee2290b | ||
|
|
b0c78910bb | ||
|
|
f4ca62b664 | ||
|
|
8eb8043a3d | ||
|
|
3e8541362a | ||
|
|
789724e348 | ||
|
|
5125b9532f | ||
|
|
ebc9de02b0 | ||
|
|
ec788fa491 | ||
|
|
9b5e264574 | ||
|
|
57c297274b | ||
|
|
e9bf092317 | ||
|
|
d173887324 | ||
|
|
99820d854c | ||
|
|
62df0a0eb2 | ||
|
|
600e9ac947 | ||
|
|
3ca41be2b4 | ||
|
|
5c7debd900 | ||
|
|
7fa5b23ce3 | ||
|
|
ff82738aaf | ||
|
|
bf5ee9d643 | ||
|
|
72a8593ecd | ||
|
|
bc3bbe07d4 | ||
|
|
c7cb64bfef | ||
|
|
629f537d06 | ||
|
|
9e988041b8 | ||
|
|
f9a8b5c9d7 | ||
|
|
b9c3538253 | ||
|
|
2bc0cdf017 | ||
|
|
02a91f60d4 | ||
|
|
fae83da197 | ||
|
|
0fe4aa6418 | ||
|
|
21a51bf0dc | ||
|
|
bcb353cc30 | ||
|
|
6af4508518 | ||
|
|
6a559bc28a | ||
|
|
0f5026cd20 | ||
|
|
a91b80a311 | ||
|
|
ec534701c8 | ||
|
|
af5169f67f | ||
|
|
18676c5e65 | ||
|
|
e2df6fda7b | ||
|
|
e9ae9782fe | ||
|
|
016dba4ca9 | ||
|
|
39c7ef305f | ||
|
|
849c1dc848 | ||
|
|
61414014fe | ||
|
|
578a915884 | ||
|
|
eacafb8a63 | ||
|
|
4446760f74 | ||
|
|
6da2a083f9 | ||
|
|
8837c8f822 | ||
|
|
bac301ed66 | ||
|
|
061db3906d | ||
|
|
fd7df5c952 | ||
|
|
a270019147 | ||
|
|
55e0209901 | ||
|
|
2b255fbbed | ||
|
|
8a2345a0fb | ||
|
|
bfa9f535aa | ||
|
|
f757623ad8 | ||
|
|
3c7465e268 |
40
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
40
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: ''
|
||||||
|
labels: bug
|
||||||
|
assignees: '9001'
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
NOTE:
|
||||||
|
all of the below are optional, consider them as inspiration, delete and rewrite at will, thx md
|
||||||
|
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
a description of what the bug is
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
List of steps to reproduce the issue, or, if it's hard to reproduce, then at least a detailed explanation of what you did to run into it
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
a description of what you expected to happen
|
||||||
|
|
||||||
|
**Screenshots**
|
||||||
|
if applicable, add screenshots to help explain your problem, such as the kickass crashpage :^)
|
||||||
|
|
||||||
|
**Server details**
|
||||||
|
if the issue is possibly on the server-side, then mention some of the following:
|
||||||
|
* server OS / version:
|
||||||
|
* python version:
|
||||||
|
* copyparty arguments:
|
||||||
|
* filesystem (`lsblk -f` on linux):
|
||||||
|
|
||||||
|
**Client details**
|
||||||
|
if the issue is possibly on the client-side, then mention some of the following:
|
||||||
|
* the device type and model:
|
||||||
|
* OS version:
|
||||||
|
* browser version:
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
any other context about the problem here
|
||||||
22
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
22
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
title: ''
|
||||||
|
labels: enhancement
|
||||||
|
assignees: '9001'
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
all of the below are optional, consider them as inspiration, delete and rewrite at will
|
||||||
|
|
||||||
|
**is your feature request related to a problem? Please describe.**
|
||||||
|
a description of what the problem is, for example, `I'm always frustrated when [...]` or `Why is it not possible to [...]`
|
||||||
|
|
||||||
|
**Describe the idea / solution you'd like**
|
||||||
|
a description of what you want to happen
|
||||||
|
|
||||||
|
**Describe any alternatives you've considered**
|
||||||
|
a description of any alternative solutions or features you've considered
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
add any other context or screenshots about the feature request here
|
||||||
10
.github/ISSUE_TEMPLATE/something-else.md
vendored
Normal file
10
.github/ISSUE_TEMPLATE/something-else.md
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
name: Something else
|
||||||
|
about: "┐(゚∀゚)┌"
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
7
.github/branch-rename.md
vendored
Normal file
7
.github/branch-rename.md
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
modernize your local checkout of the repo like so,
|
||||||
|
```sh
|
||||||
|
git branch -m master hovudstraum
|
||||||
|
git fetch origin
|
||||||
|
git branch -u origin/hovudstraum hovudstraum
|
||||||
|
git remote set-head origin -a
|
||||||
|
```
|
||||||
5
.vscode/tasks.json
vendored
5
.vscode/tasks.json
vendored
@@ -9,7 +9,10 @@
|
|||||||
{
|
{
|
||||||
"label": "no_dbg",
|
"label": "no_dbg",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "${config:python.pythonPath} .vscode/launch.py"
|
"command": "${config:python.pythonPath}",
|
||||||
|
"args": [
|
||||||
|
".vscode/launch.py"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
24
CODE_OF_CONDUCT.md
Normal file
24
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
in the words of Abraham Lincoln:
|
||||||
|
|
||||||
|
> Be excellent to each other... and... PARTY ON, DUDES!
|
||||||
|
|
||||||
|
more specifically I'll paraphrase some examples from a german automotive corporation as they cover all the bases without being too wordy
|
||||||
|
|
||||||
|
## Examples of unacceptable behavior
|
||||||
|
* intimidation, harassment, trolling
|
||||||
|
* insulting, derogatory, harmful or prejudicial comments
|
||||||
|
* posting private information without permission
|
||||||
|
* political or personal attacks
|
||||||
|
|
||||||
|
## Examples of expected behavior
|
||||||
|
* being nice, friendly, welcoming, inclusive, mindful and empathetic
|
||||||
|
* acting considerate, modest, respectful
|
||||||
|
* using polite and inclusive language
|
||||||
|
* criticize constructively and accept constructive criticism
|
||||||
|
* respect different points of view
|
||||||
|
|
||||||
|
## finally and even more specifically,
|
||||||
|
* parse opinions and feedback objectively without prejudice
|
||||||
|
* it's the message that matters, not who said it
|
||||||
|
|
||||||
|
aaand that's how you say `be nice` in a way that fills half a floppy w
|
||||||
3
CONTRIBUTING.md
Normal file
3
CONTRIBUTING.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
* do something cool
|
||||||
|
|
||||||
|
really tho, send a PR or an issue or whatever, all appreciated, anything goes, just behave aight
|
||||||
@@ -61,3 +61,8 @@ cd /mnt/nas/music/.hist
|
|||||||
~/src/copyparty/bin/dbtool.py -src up2k.*.v3 up2k.db -rm-mtp-flag -copy key
|
~/src/copyparty/bin/dbtool.py -src up2k.*.v3 up2k.db -rm-mtp-flag -copy key
|
||||||
~/src/copyparty/bin/dbtool.py -src up2k.*.v3 up2k.db -rm-mtp-flag -copy .bpm -vac
|
~/src/copyparty/bin/dbtool.py -src up2k.*.v3 up2k.db -rm-mtp-flag -copy .bpm -vac
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
# [`prisonparty.sh`](prisonparty.sh)
|
||||||
|
* run copyparty in a chroot, preventing any accidental file access
|
||||||
|
* creates bindmounts for /bin, /lib, and so on, see `sysdirs=`
|
||||||
|
|||||||
@@ -345,7 +345,7 @@ class Gateway(object):
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def sendreq(self, *args, headers={}, **kwargs):
|
def sendreq(self, meth, path, headers, **kwargs):
|
||||||
if self.password:
|
if self.password:
|
||||||
headers["Cookie"] = "=".join(["cppwd", self.password])
|
headers["Cookie"] = "=".join(["cppwd", self.password])
|
||||||
|
|
||||||
@@ -354,21 +354,21 @@ class Gateway(object):
|
|||||||
if c.rx_path:
|
if c.rx_path:
|
||||||
raise Exception()
|
raise Exception()
|
||||||
|
|
||||||
c.request(*list(args), headers=headers, **kwargs)
|
c.request(meth, path, headers=headers, **kwargs)
|
||||||
c.rx = c.getresponse()
|
c.rx = c.getresponse()
|
||||||
return c
|
return c
|
||||||
except:
|
except:
|
||||||
tid = threading.current_thread().ident
|
tid = threading.current_thread().ident
|
||||||
dbg(
|
dbg(
|
||||||
"\033[1;37;44mbad conn {:x}\n {}\n {}\033[0m".format(
|
"\033[1;37;44mbad conn {:x}\n {} {}\n {}\033[0m".format(
|
||||||
tid, " ".join(str(x) for x in args), c.rx_path if c else "(null)"
|
tid, meth, path, c.rx_path if c else "(null)"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
self.closeconn(c)
|
self.closeconn(c)
|
||||||
c = self.getconn()
|
c = self.getconn()
|
||||||
try:
|
try:
|
||||||
c.request(*list(args), headers=headers, **kwargs)
|
c.request(meth, path, headers=headers, **kwargs)
|
||||||
c.rx = c.getresponse()
|
c.rx = c.getresponse()
|
||||||
return c
|
return c
|
||||||
except:
|
except:
|
||||||
@@ -386,7 +386,7 @@ class Gateway(object):
|
|||||||
path = dewin(path)
|
path = dewin(path)
|
||||||
|
|
||||||
web_path = self.quotep("/" + "/".join([self.web_root, path])) + "?dots"
|
web_path = self.quotep("/" + "/".join([self.web_root, path])) + "?dots"
|
||||||
c = self.sendreq("GET", web_path)
|
c = self.sendreq("GET", web_path, {})
|
||||||
if c.rx.status != 200:
|
if c.rx.status != 200:
|
||||||
self.closeconn(c)
|
self.closeconn(c)
|
||||||
log(
|
log(
|
||||||
@@ -440,7 +440,7 @@ class Gateway(object):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
c = self.sendreq("GET", web_path, headers={"Range": hdr_range})
|
c = self.sendreq("GET", web_path, {"Range": hdr_range})
|
||||||
if c.rx.status != http.client.PARTIAL_CONTENT:
|
if c.rx.status != http.client.PARTIAL_CONTENT:
|
||||||
self.closeconn(c)
|
self.closeconn(c)
|
||||||
raise Exception(
|
raise Exception(
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ dependencies:
|
|||||||
|
|
||||||
note:
|
note:
|
||||||
you probably want to run this on windows clients:
|
you probably want to run this on windows clients:
|
||||||
https://github.com/9001/copyparty/blob/master/contrib/explorer-nothumbs-nofoldertypes.reg
|
https://github.com/9001/copyparty/blob/hovudstraum/contrib/explorer-nothumbs-nofoldertypes.reg
|
||||||
|
|
||||||
get server cert:
|
get server cert:
|
||||||
awk '/-BEGIN CERTIFICATE-/ {a=1} a; /-END CERTIFICATE-/{exit}' <(openssl s_client -connect 127.0.0.1:3923 </dev/null 2>/dev/null) >cert.pem
|
awk '/-BEGIN CERTIFICATE-/ {a=1} a; /-END CERTIFICATE-/{exit}' <(openssl s_client -connect 127.0.0.1:3923 </dev/null 2>/dev/null) >cert.pem
|
||||||
@@ -54,10 +54,13 @@ MACOS = platform.system() == "Darwin"
|
|||||||
info = log = dbg = None
|
info = log = dbg = None
|
||||||
|
|
||||||
|
|
||||||
print("{} v{} @ {}".format(
|
print(
|
||||||
|
"{} v{} @ {}".format(
|
||||||
platform.python_implementation(),
|
platform.python_implementation(),
|
||||||
".".join([str(x) for x in sys.version_info]),
|
".".join([str(x) for x in sys.version_info]),
|
||||||
sys.executable))
|
sys.executable,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -299,14 +302,14 @@ class Gateway(object):
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def sendreq(self, *args, headers={}, **kwargs):
|
def sendreq(self, meth, path, headers, **kwargs):
|
||||||
tid = get_tid()
|
tid = get_tid()
|
||||||
if self.password:
|
if self.password:
|
||||||
headers["Cookie"] = "=".join(["cppwd", self.password])
|
headers["Cookie"] = "=".join(["cppwd", self.password])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
c = self.getconn(tid)
|
c = self.getconn(tid)
|
||||||
c.request(*list(args), headers=headers, **kwargs)
|
c.request(meth, path, headers=headers, **kwargs)
|
||||||
return c.getresponse()
|
return c.getresponse()
|
||||||
except:
|
except:
|
||||||
dbg("bad conn")
|
dbg("bad conn")
|
||||||
@@ -314,7 +317,7 @@ class Gateway(object):
|
|||||||
self.closeconn(tid)
|
self.closeconn(tid)
|
||||||
try:
|
try:
|
||||||
c = self.getconn(tid)
|
c = self.getconn(tid)
|
||||||
c.request(*list(args), headers=headers, **kwargs)
|
c.request(meth, path, headers=headers, **kwargs)
|
||||||
return c.getresponse()
|
return c.getresponse()
|
||||||
except:
|
except:
|
||||||
info("http connection failed:\n" + traceback.format_exc())
|
info("http connection failed:\n" + traceback.format_exc())
|
||||||
@@ -331,7 +334,7 @@ class Gateway(object):
|
|||||||
path = dewin(path)
|
path = dewin(path)
|
||||||
|
|
||||||
web_path = self.quotep("/" + "/".join([self.web_root, path])) + "?dots&ls"
|
web_path = self.quotep("/" + "/".join([self.web_root, path])) + "?dots&ls"
|
||||||
r = self.sendreq("GET", web_path)
|
r = self.sendreq("GET", web_path, {})
|
||||||
if r.status != 200:
|
if r.status != 200:
|
||||||
self.closeconn()
|
self.closeconn()
|
||||||
log(
|
log(
|
||||||
@@ -368,7 +371,7 @@ class Gateway(object):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
r = self.sendreq("GET", web_path, headers={"Range": hdr_range})
|
r = self.sendreq("GET", web_path, {"Range": hdr_range})
|
||||||
if r.status != http.client.PARTIAL_CONTENT:
|
if r.status != http.client.PARTIAL_CONTENT:
|
||||||
self.closeconn()
|
self.closeconn()
|
||||||
raise Exception(
|
raise Exception(
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ some of these rely on libraries which are not MIT-compatible
|
|||||||
|
|
||||||
* [audio-bpm.py](./audio-bpm.py) detects the BPM of music using the BeatRoot Vamp Plugin; imports GPL2
|
* [audio-bpm.py](./audio-bpm.py) detects the BPM of music using the BeatRoot Vamp Plugin; imports GPL2
|
||||||
* [audio-key.py](./audio-key.py) detects the melodic key of music using the Mixxx fork of keyfinder; imports GPL3
|
* [audio-key.py](./audio-key.py) detects the melodic key of music using the Mixxx fork of keyfinder; imports GPL3
|
||||||
|
* [media-hash.py](./media-hash.py) generates checksums for audio and video streams; uses FFmpeg (LGPL or GPL)
|
||||||
|
|
||||||
|
|
||||||
# dependencies
|
# dependencies
|
||||||
@@ -18,7 +19,10 @@ run [`install-deps.sh`](install-deps.sh) to build/install most dependencies requ
|
|||||||
|
|
||||||
# usage from copyparty
|
# usage from copyparty
|
||||||
|
|
||||||
`copyparty -e2dsa -e2ts -mtp key=f,audio-key.py -mtp .bpm=f,audio-bpm.py`
|
`copyparty -e2dsa -e2ts` followed by any combination of these:
|
||||||
|
* `-mtp key=f,audio-key.py`
|
||||||
|
* `-mtp .bpm=f,audio-bpm.py`
|
||||||
|
* `-mtp ahash,vhash=f,media-hash.py`
|
||||||
|
|
||||||
* `f,` makes the detected value replace any existing values
|
* `f,` makes the detected value replace any existing values
|
||||||
* the `.` in `.bpm` indicates numeric value
|
* the `.` in `.bpm` indicates numeric value
|
||||||
@@ -29,6 +33,9 @@ run [`install-deps.sh`](install-deps.sh) to build/install most dependencies requ
|
|||||||
## usage with volume-flags
|
## usage with volume-flags
|
||||||
|
|
||||||
instead of affecting all volumes, you can set the options for just one volume like so:
|
instead of affecting all volumes, you can set the options for just one volume like so:
|
||||||
```
|
|
||||||
copyparty -v /mnt/nas/music:/music:r:cmtp=key=f,audio-key.py:cmtp=.bpm=f,audio-bpm.py:ce2dsa:ce2ts
|
`copyparty -v /mnt/nas/music:/music:r:c,e2dsa:c,e2ts` immediately followed by any combination of these:
|
||||||
```
|
|
||||||
|
* `:c,mtp=key=f,audio-key.py`
|
||||||
|
* `:c,mtp=.bpm=f,audio-bpm.py`
|
||||||
|
* `:c,mtp=ahash,vhash=f,media-hash.py`
|
||||||
|
|||||||
73
bin/mtag/media-hash.py
Normal file
73
bin/mtag/media-hash.py
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import base64
|
||||||
|
import hashlib
|
||||||
|
import subprocess as sp
|
||||||
|
|
||||||
|
try:
|
||||||
|
from copyparty.util import fsenc
|
||||||
|
except:
|
||||||
|
|
||||||
|
def fsenc(p):
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
dep: ffmpeg
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def det():
|
||||||
|
# fmt: off
|
||||||
|
cmd = [
|
||||||
|
"ffmpeg",
|
||||||
|
"-nostdin",
|
||||||
|
"-hide_banner",
|
||||||
|
"-v", "fatal",
|
||||||
|
"-i", fsenc(sys.argv[1]),
|
||||||
|
"-f", "framemd5",
|
||||||
|
"-"
|
||||||
|
]
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
|
p = sp.Popen(cmd, stdout=sp.PIPE)
|
||||||
|
# ps = io.TextIOWrapper(p.stdout, encoding="utf-8")
|
||||||
|
ps = p.stdout
|
||||||
|
|
||||||
|
chans = {}
|
||||||
|
for ln in ps:
|
||||||
|
if ln.startswith(b"#stream#"):
|
||||||
|
break
|
||||||
|
|
||||||
|
m = re.match(r"^#media_type ([0-9]): ([a-zA-Z])", ln.decode("utf-8"))
|
||||||
|
if m:
|
||||||
|
chans[m.group(1)] = m.group(2)
|
||||||
|
|
||||||
|
hashers = [hashlib.sha512(), hashlib.sha512()]
|
||||||
|
for ln in ps:
|
||||||
|
n = int(ln[:1])
|
||||||
|
v = ln.rsplit(b",", 1)[-1].strip()
|
||||||
|
hashers[n].update(v)
|
||||||
|
|
||||||
|
r = {}
|
||||||
|
for k, v in chans.items():
|
||||||
|
dg = hashers[int(k)].digest()[:12]
|
||||||
|
dg = base64.urlsafe_b64encode(dg).decode("ascii")
|
||||||
|
r[v[0].lower() + "hash"] = dg
|
||||||
|
|
||||||
|
print(json.dumps(r, indent=4))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
try:
|
||||||
|
det()
|
||||||
|
except:
|
||||||
|
pass # mute
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
39
bin/mtag/res/yt-ipr.conf
Normal file
39
bin/mtag/res/yt-ipr.conf
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# example config file to use copyparty as a youtube manifest collector,
|
||||||
|
# use with copyparty like: python copyparty.py -c yt-ipr.conf
|
||||||
|
#
|
||||||
|
# see docs/example.conf for a better explanation of the syntax, but
|
||||||
|
# newlines are block separators, so adding blank lines inside a volume definition is bad
|
||||||
|
# (use comments as separators instead)
|
||||||
|
|
||||||
|
|
||||||
|
# create user ed, password wark
|
||||||
|
u ed:wark
|
||||||
|
|
||||||
|
|
||||||
|
# create a volume at /ytm which stores files at ./srv/ytm
|
||||||
|
./srv/ytm
|
||||||
|
/ytm
|
||||||
|
# write-only, but read-write for user ed
|
||||||
|
w
|
||||||
|
rw ed
|
||||||
|
# rescan the volume on startup
|
||||||
|
c e2dsa
|
||||||
|
# collect tags from all new files since last scan
|
||||||
|
c e2ts
|
||||||
|
# optionally enable compression to make the files 50% smaller
|
||||||
|
c pk
|
||||||
|
# only allow uploads which are between 16k and 1m large
|
||||||
|
c sz=16k-1m
|
||||||
|
# allow up to 10 uploads over 5 minutes from each ip
|
||||||
|
c maxn=10,300
|
||||||
|
# move uploads into subfolders: YEAR-MONTH / DAY-HOUR / <upload>
|
||||||
|
c rotf=%Y-%m/%d-%H
|
||||||
|
# delete uploads when they are 24 hours old
|
||||||
|
c lifetime=86400
|
||||||
|
# add the parser and tell copyparty what tags it can expect from it
|
||||||
|
c mtp=yt-id,yt-title,yt-author,yt-channel,yt-views,yt-private,yt-manifest,yt-expires=bin/mtag/yt-ipr.py
|
||||||
|
# decide which tags we want to index and in what order
|
||||||
|
c mte=yt-id,yt-title,yt-author,yt-channel,yt-views,yt-private,yt-manifest,yt-expires
|
||||||
|
|
||||||
|
|
||||||
|
# create any other volumes you'd like down here, or merge this with an existing config file
|
||||||
47
bin/mtag/res/yt-ipr.user.js
Normal file
47
bin/mtag/res/yt-ipr.user.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
// ==UserScript==
|
||||||
|
// @name youtube-playerdata-hub
|
||||||
|
// @match https://youtube.com/*
|
||||||
|
// @match https://*.youtube.com/*
|
||||||
|
// @version 1.0
|
||||||
|
// @grant GM_addStyle
|
||||||
|
// ==/UserScript==
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
var server = 'https://127.0.0.1:3923/ytm?pw=wark',
|
||||||
|
interval = 60; // sec
|
||||||
|
|
||||||
|
var sent = {};
|
||||||
|
function send(txt, mf_url, desc) {
|
||||||
|
if (sent[mf_url])
|
||||||
|
return;
|
||||||
|
|
||||||
|
fetch(server + '&_=' + Date.now(), { method: "PUT", body: txt });
|
||||||
|
console.log('[yt-pdh] yeet %d bytes, %s', txt.length, desc);
|
||||||
|
sent[mf_url] = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collect() {
|
||||||
|
try {
|
||||||
|
var pd = document.querySelector('ytd-watch-flexy');
|
||||||
|
if (!pd)
|
||||||
|
return console.log('[yt-pdh] no video found');
|
||||||
|
|
||||||
|
pd = pd.playerData;
|
||||||
|
var mu = pd.streamingData.dashManifestUrl || pd.streamingData.hlsManifestUrl;
|
||||||
|
if (!mu || !mu.length)
|
||||||
|
return console.log('[yt-pdh] no manifest found');
|
||||||
|
|
||||||
|
var desc = pd.videoDetails.videoId + ', ' + pd.videoDetails.title;
|
||||||
|
send(JSON.stringify(pd), mu, desc);
|
||||||
|
}
|
||||||
|
catch (ex) {
|
||||||
|
console.log("[yt-pdh]", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setInterval(collect, interval * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
var scr = document.createElement('script');
|
||||||
|
scr.textContent = '(' + main.toString() + ')();';
|
||||||
|
(document.head || document.getElementsByTagName('head')[0]).appendChild(scr);
|
||||||
|
console.log('[yt-pdh] a');
|
||||||
198
bin/mtag/yt-ipr.py
Normal file
198
bin/mtag/yt-ipr.py
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
import re
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import gzip
|
||||||
|
import json
|
||||||
|
import base64
|
||||||
|
import string
|
||||||
|
import urllib.request
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
"""
|
||||||
|
youtube initial player response
|
||||||
|
|
||||||
|
it's probably best to use this through a config file; see res/yt-ipr.conf
|
||||||
|
|
||||||
|
but if you want to use plain arguments instead then:
|
||||||
|
-v srv/ytm:ytm:w:rw,ed
|
||||||
|
:c,e2ts:c,e2dsa
|
||||||
|
:c,sz=16k-1m:c,maxn=10,300:c,rotf=%Y-%m/%d-%H
|
||||||
|
:c,mtp=yt-id,yt-title,yt-author,yt-channel,yt-views,yt-private,yt-manifest,yt-expires=bin/mtag/yt-ipr.py
|
||||||
|
:c,mte=yt-id,yt-title,yt-author,yt-channel,yt-views,yt-private,yt-manifest,yt-expires
|
||||||
|
|
||||||
|
see res/yt-ipr.user.js for the example userscript to go with this
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
try:
|
||||||
|
with gzip.open(sys.argv[1], "rt", encoding="utf-8", errors="replace") as f:
|
||||||
|
txt = f.read()
|
||||||
|
except:
|
||||||
|
with open(sys.argv[1], "r", encoding="utf-8", errors="replace") as f:
|
||||||
|
txt = f.read()
|
||||||
|
|
||||||
|
txt = "{" + txt.split("{", 1)[1]
|
||||||
|
|
||||||
|
try:
|
||||||
|
pd = json.loads(txt)
|
||||||
|
except json.decoder.JSONDecodeError as ex:
|
||||||
|
pd = json.loads(txt[: ex.pos])
|
||||||
|
|
||||||
|
# print(json.dumps(pd, indent=2))
|
||||||
|
|
||||||
|
if "videoDetails" in pd:
|
||||||
|
parse_youtube(pd)
|
||||||
|
else:
|
||||||
|
parse_freg(pd)
|
||||||
|
|
||||||
|
|
||||||
|
def get_expiration(url):
|
||||||
|
et = re.search(r"[?&]expire=([0-9]+)", url).group(1)
|
||||||
|
et = datetime.utcfromtimestamp(int(et))
|
||||||
|
return et.strftime("%Y-%m-%d, %H:%M")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_youtube(pd):
|
||||||
|
vd = pd["videoDetails"]
|
||||||
|
sd = pd["streamingData"]
|
||||||
|
|
||||||
|
et = sd["adaptiveFormats"][0]["url"]
|
||||||
|
et = get_expiration(et)
|
||||||
|
|
||||||
|
mf = []
|
||||||
|
if "dashManifestUrl" in sd:
|
||||||
|
mf.append("dash")
|
||||||
|
if "hlsManifestUrl" in sd:
|
||||||
|
mf.append("hls")
|
||||||
|
|
||||||
|
r = {
|
||||||
|
"yt-id": vd["videoId"],
|
||||||
|
"yt-title": vd["title"],
|
||||||
|
"yt-author": vd["author"],
|
||||||
|
"yt-channel": vd["channelId"],
|
||||||
|
"yt-views": vd["viewCount"],
|
||||||
|
"yt-private": vd["isPrivate"],
|
||||||
|
# "yt-expires": sd["expiresInSeconds"],
|
||||||
|
"yt-manifest": ",".join(mf),
|
||||||
|
"yt-expires": et,
|
||||||
|
}
|
||||||
|
print(json.dumps(r))
|
||||||
|
|
||||||
|
freg_conv(pd)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_freg(pd):
|
||||||
|
md = pd["metadata"]
|
||||||
|
r = {
|
||||||
|
"yt-id": md["id"],
|
||||||
|
"yt-title": md["title"],
|
||||||
|
"yt-author": md["channelName"],
|
||||||
|
"yt-channel": md["channelURL"].strip("/").split("/")[-1],
|
||||||
|
"yt-expires": get_expiration(list(pd["video"].values())[0]),
|
||||||
|
}
|
||||||
|
print(json.dumps(r))
|
||||||
|
|
||||||
|
|
||||||
|
def freg_conv(pd):
|
||||||
|
# based on getURLs.js v1.5 (2021-08-07)
|
||||||
|
# fmt: off
|
||||||
|
priority = {
|
||||||
|
"video": [
|
||||||
|
337, 315, 266, 138, # 2160p60
|
||||||
|
313, 336, # 2160p
|
||||||
|
308, # 1440p60
|
||||||
|
271, 264, # 1440p
|
||||||
|
335, 303, 299, # 1080p60
|
||||||
|
248, 169, 137, # 1080p
|
||||||
|
334, 302, 298, # 720p60
|
||||||
|
247, 136 # 720p
|
||||||
|
],
|
||||||
|
"audio": [
|
||||||
|
251, 141, 171, 140, 250, 249, 139
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
vid_id = pd["videoDetails"]["videoId"]
|
||||||
|
chan_id = pd["videoDetails"]["channelId"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
thumb_url = pd["microformat"]["playerMicroformatRenderer"]["thumbnail"]["thumbnails"][0]["url"]
|
||||||
|
start_ts = pd["microformat"]["playerMicroformatRenderer"]["liveBroadcastDetails"]["startTimestamp"]
|
||||||
|
except:
|
||||||
|
thumb_url = f"https://img.youtube.com/vi/{vid_id}/maxresdefault.jpg"
|
||||||
|
start_ts = ""
|
||||||
|
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
|
metadata = {
|
||||||
|
"title": pd["videoDetails"]["title"],
|
||||||
|
"id": vid_id,
|
||||||
|
"channelName": pd["videoDetails"]["author"],
|
||||||
|
"channelURL": "https://www.youtube.com/channel/" + chan_id,
|
||||||
|
"description": pd["videoDetails"]["shortDescription"],
|
||||||
|
"thumbnailUrl": thumb_url,
|
||||||
|
"startTimestamp": start_ts,
|
||||||
|
}
|
||||||
|
|
||||||
|
if [x for x in vid_id if x not in string.ascii_letters + string.digits + "_-"]:
|
||||||
|
print(f"malicious json", file=sys.stderr)
|
||||||
|
return
|
||||||
|
|
||||||
|
basepath = os.path.dirname(sys.argv[1])
|
||||||
|
|
||||||
|
thumb_fn = f"{basepath}/{vid_id}.jpg"
|
||||||
|
tmp_fn = f"{thumb_fn}.{os.getpid()}"
|
||||||
|
if not os.path.exists(thumb_fn) and (
|
||||||
|
thumb_url.startswith("https://img.youtube.com/vi/")
|
||||||
|
or thumb_url.startswith("https://i.ytimg.com/vi/")
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(thumb_url) as fi:
|
||||||
|
with open(tmp_fn, "wb") as fo:
|
||||||
|
fo.write(fi.read())
|
||||||
|
|
||||||
|
os.rename(tmp_fn, thumb_fn)
|
||||||
|
except:
|
||||||
|
if os.path.exists(tmp_fn):
|
||||||
|
os.unlink(tmp_fn)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(thumb_fn, "rb") as f:
|
||||||
|
thumb = base64.b64encode(f.read()).decode("ascii")
|
||||||
|
except:
|
||||||
|
thumb = "/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/yQALCAABAAEBAREA/8wABgAQEAX/2gAIAQEAAD8A0s8g/9k="
|
||||||
|
|
||||||
|
metadata["thumbnail"] = "data:image/jpeg;base64," + thumb
|
||||||
|
|
||||||
|
ret = {
|
||||||
|
"metadata": metadata,
|
||||||
|
"version": "1.5",
|
||||||
|
"createTime": datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||||
|
}
|
||||||
|
|
||||||
|
for stream, itags in priority.items():
|
||||||
|
for itag in itags:
|
||||||
|
url = None
|
||||||
|
for afmt in pd["streamingData"]["adaptiveFormats"]:
|
||||||
|
if itag == afmt["itag"]:
|
||||||
|
url = afmt["url"]
|
||||||
|
break
|
||||||
|
|
||||||
|
if url:
|
||||||
|
ret[stream] = {itag: url}
|
||||||
|
break
|
||||||
|
|
||||||
|
fn = f"{basepath}/{vid_id}.urls.json"
|
||||||
|
with open(fn, "w", encoding="utf-8", errors="replace") as f:
|
||||||
|
f.write(json.dumps(ret, indent=4))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except:
|
||||||
|
# raise
|
||||||
|
pass
|
||||||
99
bin/prisonparty.sh
Normal file
99
bin/prisonparty.sh
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# runs copyparty (or any other program really) in a chroot
|
||||||
|
#
|
||||||
|
# assumption: these directories, and everything within, are owned by root
|
||||||
|
sysdirs=( /bin /lib /lib32 /lib64 /sbin /usr )
|
||||||
|
|
||||||
|
|
||||||
|
# error-handler
|
||||||
|
help() { cat <<'EOF'
|
||||||
|
|
||||||
|
usage:
|
||||||
|
./prisonparty.sh <ROOTDIR> <UID> <GID> [VOLDIR [VOLDIR...]] -- copyparty-sfx.py [...]"
|
||||||
|
|
||||||
|
example:
|
||||||
|
./prisonparty.sh /var/lib/copyparty-jail 1000 1000 /mnt/nas/music -- copyparty-sfx.py -v /mnt/nas/music::rwmd"
|
||||||
|
|
||||||
|
EOF
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# read arguments
|
||||||
|
trap help EXIT
|
||||||
|
jail="$(realpath "$1")"; shift
|
||||||
|
uid="$1"; shift
|
||||||
|
gid="$1"; shift
|
||||||
|
|
||||||
|
vols=()
|
||||||
|
while true; do
|
||||||
|
v="$1"; shift
|
||||||
|
[ "$v" = -- ] && break # end of volumes
|
||||||
|
[ "$#" -eq 0 ] && break # invalid usage
|
||||||
|
vols+=( "$(realpath "$v")" )
|
||||||
|
done
|
||||||
|
pybin="$1"; shift
|
||||||
|
pybin="$(realpath "$pybin")"
|
||||||
|
cpp="$1"; shift
|
||||||
|
cpp="$(realpath "$cpp")"
|
||||||
|
cppdir="$(dirname "$cpp")"
|
||||||
|
trap - EXIT
|
||||||
|
|
||||||
|
|
||||||
|
# debug/vis
|
||||||
|
echo
|
||||||
|
echo "chroot-dir = $jail"
|
||||||
|
echo "user:group = $uid:$gid"
|
||||||
|
echo " copyparty = $cpp"
|
||||||
|
echo
|
||||||
|
printf '\033[33m%s\033[0m\n' "copyparty can access these folders and all their subdirectories:"
|
||||||
|
for v in "${vols[@]}"; do
|
||||||
|
printf '\033[36m ├─\033[0m %s \033[36m ── added by (You)\033[0m\n' "$v"
|
||||||
|
done
|
||||||
|
printf '\033[36m ├─\033[0m %s \033[36m ── where the copyparty binary is\033[0m\n' "$cppdir"
|
||||||
|
printf '\033[36m ╰─\033[0m %s \033[36m ── the folder you are currently in\033[0m\n' "$PWD"
|
||||||
|
vols+=("$cppdir" "$PWD")
|
||||||
|
echo
|
||||||
|
|
||||||
|
|
||||||
|
# remove any trailing slashes
|
||||||
|
jail="${jail%/}"
|
||||||
|
cppdir="${cppdir%/}"
|
||||||
|
|
||||||
|
|
||||||
|
# bind-mount system directories and volumes
|
||||||
|
printf '%s\n' "${sysdirs[@]}" "${vols[@]}" | LC_ALL=C sort |
|
||||||
|
while IFS= read -r v; do
|
||||||
|
[ -e "$v" ] || {
|
||||||
|
# printf '\033[1;31mfolder does not exist:\033[0m %s\n' "/$v"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
i1=$(stat -c%D.%i "$v" 2>/dev/null || echo a)
|
||||||
|
i2=$(stat -c%D.%i "$jail$v" 2>/dev/null || echo b)
|
||||||
|
[ $i1 = $i2 ] && continue
|
||||||
|
|
||||||
|
mkdir -p "$jail$v"
|
||||||
|
mount --bind "$v" "$jail$v"
|
||||||
|
done
|
||||||
|
|
||||||
|
|
||||||
|
# create a tmp
|
||||||
|
mkdir -p "$jail/tmp"
|
||||||
|
chmod 777 "$jail/tmp"
|
||||||
|
|
||||||
|
|
||||||
|
# run copyparty
|
||||||
|
/sbin/chroot --userspec=$uid:$gid "$jail" "$pybin" "$cpp" "$@" && rv=0 || rv=$?
|
||||||
|
|
||||||
|
|
||||||
|
# cleanup if not in use
|
||||||
|
lsof "$jail" | grep -qF "$jail" &&
|
||||||
|
echo "chroot is in use, will not cleanup" ||
|
||||||
|
{
|
||||||
|
mount | grep -qF " on $jail" |
|
||||||
|
awk '{sub(/ type .*/,"");sub(/.* on /,"");print}' |
|
||||||
|
LC_ALL=C sort -r | tee /dev/stderr | tr '\n' '\0' | xargs -r0 umount
|
||||||
|
}
|
||||||
|
exit $rv
|
||||||
@@ -29,7 +29,8 @@ however if your copyparty is behind a reverse-proxy, you may want to use [`share
|
|||||||
|
|
||||||
# OS integration
|
# OS integration
|
||||||
init-scripts to start copyparty as a service
|
init-scripts to start copyparty as a service
|
||||||
* [`systemd/copyparty.service`](systemd/copyparty.service)
|
* [`systemd/copyparty.service`](systemd/copyparty.service) runs the sfx normally
|
||||||
|
* [`systemd/prisonparty.service`](systemd/prisonparty.service) runs the sfx in a chroot
|
||||||
* [`openrc/copyparty`](openrc/copyparty)
|
* [`openrc/copyparty`](openrc/copyparty)
|
||||||
|
|
||||||
# Reverse-proxy
|
# Reverse-proxy
|
||||||
|
|||||||
@@ -8,11 +8,11 @@
|
|||||||
#
|
#
|
||||||
# you may want to:
|
# you may want to:
|
||||||
# change '/usr/bin/python' to another interpreter
|
# change '/usr/bin/python' to another interpreter
|
||||||
# change '/mnt::a' to another location or permission-set
|
# change '/mnt::rw' to another location or permission-set
|
||||||
|
|
||||||
name="$SVCNAME"
|
name="$SVCNAME"
|
||||||
command_background=true
|
command_background=true
|
||||||
pidfile="/var/run/$SVCNAME.pid"
|
pidfile="/var/run/$SVCNAME.pid"
|
||||||
|
|
||||||
command="/usr/bin/python /usr/local/bin/copyparty-sfx.py"
|
command="/usr/bin/python /usr/local/bin/copyparty-sfx.py"
|
||||||
command_args="-q -v /mnt::a"
|
command_args="-q -v /mnt::rw"
|
||||||
|
|||||||
@@ -6,13 +6,28 @@
|
|||||||
#
|
#
|
||||||
# you may want to:
|
# you may want to:
|
||||||
# change '/usr/bin/python' to another interpreter
|
# change '/usr/bin/python' to another interpreter
|
||||||
# change '/mnt::a' to another location or permission-set
|
# change '/mnt::rw' to another location or permission-set
|
||||||
|
#
|
||||||
|
# with `Type=notify`, copyparty will signal systemd when it is ready to
|
||||||
|
# accept connections; correctly delaying units depending on copyparty.
|
||||||
|
# But note that journalctl will get the timestamps wrong due to
|
||||||
|
# python disabling line-buffering, so messages are out-of-order:
|
||||||
|
# https://user-images.githubusercontent.com/241032/126040249-cb535cc7-c599-4931-a796-a5d9af691bad.png
|
||||||
|
#
|
||||||
|
# enable line-buffering for realtime logging (slight performance cost):
|
||||||
|
# modify ExecStart and prefix it with `/usr/bin/stdbuf -oL` like so:
|
||||||
|
# ExecStart=/usr/bin/stdbuf -oL /usr/bin/python3 [...]
|
||||||
|
# but some systemd versions require this instead (higher performance cost):
|
||||||
|
# inside the [Service] block, add the following line:
|
||||||
|
# Environment=PYTHONUNBUFFERED=x
|
||||||
|
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=copyparty file server
|
Description=copyparty file server
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
ExecStart=/usr/bin/python3 /usr/local/bin/copyparty-sfx.py -q -v /mnt::a
|
Type=notify
|
||||||
|
SyslogIdentifier=copyparty
|
||||||
|
ExecStart=/usr/bin/python3 /usr/local/bin/copyparty-sfx.py -q -v /mnt::rw
|
||||||
ExecStartPre=/bin/bash -c 'mkdir -p /run/tmpfiles.d/ && echo "x /tmp/pe-copyparty*" > /run/tmpfiles.d/copyparty.conf'
|
ExecStartPre=/bin/bash -c 'mkdir -p /run/tmpfiles.d/ && echo "x /tmp/pe-copyparty*" > /run/tmpfiles.d/copyparty.conf'
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
|
|||||||
27
contrib/systemd/prisonparty.service
Normal file
27
contrib/systemd/prisonparty.service
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# this will start `/usr/local/bin/copyparty-sfx.py`
|
||||||
|
# in a chroot, preventing accidental access elsewhere
|
||||||
|
# and share '/mnt' with anonymous read+write
|
||||||
|
#
|
||||||
|
# installation:
|
||||||
|
# 1) put copyparty-sfx.py and prisonparty.sh in /usr/local/bin
|
||||||
|
# 2) cp -pv prisonparty.service /etc/systemd/system && systemctl enable --now prisonparty
|
||||||
|
#
|
||||||
|
# you may want to:
|
||||||
|
# change '/mnt::rw' to another location or permission-set
|
||||||
|
# (remember to change the '/mnt' chroot arg too)
|
||||||
|
#
|
||||||
|
# enable line-buffering for realtime logging (slight performance cost):
|
||||||
|
# inside the [Service] block, add the following line:
|
||||||
|
# Environment=PYTHONUNBUFFERED=x
|
||||||
|
|
||||||
|
[Unit]
|
||||||
|
Description=copyparty file server
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
SyslogIdentifier=prisonparty
|
||||||
|
WorkingDirectory=/usr/local/bin
|
||||||
|
ExecStart=/bin/bash /usr/local/bin/prisonparty.sh /var/lib/copyparty-jail 1000 1000 /mnt -- \
|
||||||
|
/usr/bin/python3 /usr/local/bin/copyparty-sfx.py -q -v /mnt::rw
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -25,6 +25,28 @@ ANYWIN = WINDOWS or sys.platform in ["msys"]
|
|||||||
MACOS = platform.system() == "Darwin"
|
MACOS = platform.system() == "Darwin"
|
||||||
|
|
||||||
|
|
||||||
|
def get_unix_home():
|
||||||
|
try:
|
||||||
|
v = os.environ["XDG_CONFIG_HOME"]
|
||||||
|
if not v:
|
||||||
|
raise Exception()
|
||||||
|
ret = os.path.normpath(v)
|
||||||
|
os.listdir(ret)
|
||||||
|
return ret
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
v = os.path.expanduser("~/.config")
|
||||||
|
if v.startswith("~"):
|
||||||
|
raise Exception()
|
||||||
|
ret = os.path.normpath(v)
|
||||||
|
os.listdir(ret)
|
||||||
|
return ret
|
||||||
|
except:
|
||||||
|
return "/tmp"
|
||||||
|
|
||||||
|
|
||||||
class EnvParams(object):
|
class EnvParams(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.t0 = time.time()
|
self.t0 = time.time()
|
||||||
@@ -37,10 +59,7 @@ class EnvParams(object):
|
|||||||
elif sys.platform == "darwin":
|
elif sys.platform == "darwin":
|
||||||
self.cfg = os.path.expanduser("~/Library/Preferences/copyparty")
|
self.cfg = os.path.expanduser("~/Library/Preferences/copyparty")
|
||||||
else:
|
else:
|
||||||
self.cfg = os.path.normpath(
|
self.cfg = get_unix_home() + "/copyparty"
|
||||||
os.getenv("XDG_CONFIG_HOME", os.path.expanduser("~/.config"))
|
|
||||||
+ "/copyparty"
|
|
||||||
)
|
|
||||||
|
|
||||||
self.cfg = self.cfg.replace("\\", "/")
|
self.cfg = self.cfg.replace("\\", "/")
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ from textwrap import dedent
|
|||||||
from .__init__ import E, WINDOWS, VT100, PY2, unicode
|
from .__init__ import E, WINDOWS, VT100, PY2, unicode
|
||||||
from .__version__ import S_VERSION, S_BUILD_DT, CODENAME
|
from .__version__ import S_VERSION, S_BUILD_DT, CODENAME
|
||||||
from .svchub import SvcHub
|
from .svchub import SvcHub
|
||||||
from .util import py_desc, align_tab, IMPLICATIONS, alltrace
|
from .util import py_desc, align_tab, IMPLICATIONS, ansi_re
|
||||||
|
from .authsrv import re_vol
|
||||||
|
|
||||||
HAVE_SSL = True
|
HAVE_SSL = True
|
||||||
try:
|
try:
|
||||||
@@ -66,8 +67,12 @@ class Dodge11874(RiceFormatter):
|
|||||||
def lprint(*a, **ka):
|
def lprint(*a, **ka):
|
||||||
global printed
|
global printed
|
||||||
|
|
||||||
printed += " ".join(unicode(x) for x in a) + ka.get("end", "\n")
|
txt = " ".join(unicode(x) for x in a) + ka.get("end", "\n")
|
||||||
print(*a, **ka)
|
printed += txt
|
||||||
|
if not VT100:
|
||||||
|
txt = ansi_re.sub("", txt)
|
||||||
|
|
||||||
|
print(txt, **ka)
|
||||||
|
|
||||||
|
|
||||||
def warn(msg):
|
def warn(msg):
|
||||||
@@ -191,42 +196,40 @@ def sighandler(sig=None, frame=None):
|
|||||||
print("\n".join(msg))
|
print("\n".join(msg))
|
||||||
|
|
||||||
|
|
||||||
def stackmon(fp, ival):
|
|
||||||
ctr = 0
|
|
||||||
while True:
|
|
||||||
ctr += 1
|
|
||||||
time.sleep(ival)
|
|
||||||
st = "{}, {}\n{}".format(ctr, time.time(), alltrace())
|
|
||||||
with open(fp, "wb") as f:
|
|
||||||
f.write(st.encode("utf-8", "replace"))
|
|
||||||
|
|
||||||
|
|
||||||
def run_argparse(argv, formatter):
|
def run_argparse(argv, formatter):
|
||||||
ap = argparse.ArgumentParser(
|
ap = argparse.ArgumentParser(
|
||||||
formatter_class=formatter,
|
formatter_class=formatter,
|
||||||
prog="copyparty",
|
prog="copyparty",
|
||||||
description="http file sharing hub v{} ({})".format(S_VERSION, S_BUILD_DT),
|
description="http file sharing hub v{} ({})".format(S_VERSION, S_BUILD_DT),
|
||||||
epilog=dedent(
|
)
|
||||||
|
|
||||||
|
sects = [
|
||||||
|
[
|
||||||
|
"accounts",
|
||||||
|
"accounts and volumes",
|
||||||
|
dedent(
|
||||||
"""
|
"""
|
||||||
-a takes username:password,
|
-a takes username:password,
|
||||||
-v takes src:dst:permset:permset:cflag:cflag:...
|
-v takes src:dst:perm1:perm2:permN:volflag1:volflag2:volflagN:...
|
||||||
where "permset" is accesslevel followed by username (no separator)
|
where "perm" is "accesslevels,username1,username2,..."
|
||||||
and "cflag" is config flags to set on this volume
|
and "volflag" is config flags to set on this volume
|
||||||
|
|
||||||
list of cflags:
|
list of accesslevels:
|
||||||
"cnodupe" rejects existing files (instead of symlinking them)
|
"r" (read): list folder contents, download files
|
||||||
"ce2d" sets -e2d (all -e2* args can be set using ce2* cflags)
|
"w" (write): upload files; need "r" to see the uploads
|
||||||
"cd2t" disables metadata collection, overrides -e2t*
|
"m" (move): move files and folders; need "w" at destination
|
||||||
"cd2d" disables all database stuff, overrides -e2*
|
"d" (delete): permanently delete files and folders
|
||||||
|
|
||||||
|
too many volflags to list here, see the other sections
|
||||||
|
|
||||||
example:\033[35m
|
example:\033[35m
|
||||||
-a ed:hunter2 -v .::r:aed -v ../inc:dump:w:aed:cnodupe \033[36m
|
-a ed:hunter2 -v .::r:rw,ed -v ../inc:dump:w:rw,ed:c,nodupe \033[36m
|
||||||
mount current directory at "/" with
|
mount current directory at "/" with
|
||||||
* r (read-only) for everyone
|
* r (read-only) for everyone
|
||||||
* a (read+write) for ed
|
* rw (read+write) for ed
|
||||||
mount ../inc at "/dump" with
|
mount ../inc at "/dump" with
|
||||||
* w (write-only) for everyone
|
* w (write-only) for everyone
|
||||||
* a (read+write) for ed
|
* rw (read+write) for ed
|
||||||
* reject duplicate files \033[0m
|
* reject duplicate files \033[0m
|
||||||
|
|
||||||
if no accounts or volumes are configured,
|
if no accounts or volumes are configured,
|
||||||
@@ -234,73 +237,142 @@ def run_argparse(argv, formatter):
|
|||||||
|
|
||||||
consider the config file for more flexible account/volume management,
|
consider the config file for more flexible account/volume management,
|
||||||
including dynamic reload at runtime (and being more readable w)
|
including dynamic reload at runtime (and being more readable w)
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"flags",
|
||||||
|
"list of volflags",
|
||||||
|
dedent(
|
||||||
|
"""
|
||||||
|
volflags are appended to volume definitions, for example,
|
||||||
|
to create a write-only volume with the \033[33mnodupe\033[0m and \033[32mnosub\033[0m flags:
|
||||||
|
\033[35m-v /mnt/inc:/inc:w\033[33m:c,nodupe\033[32m:c,nosub
|
||||||
|
|
||||||
|
\033[0muploads, general:
|
||||||
|
\033[36mnodupe\033[35m rejects existing files (instead of symlinking them)
|
||||||
|
\033[36mnosub\033[35m forces all uploads into the top folder of the vfs
|
||||||
|
\033[36mgz\033[35m allows server-side gzip of uploads with ?gz (also c,xz)
|
||||||
|
\033[36mpk\033[35m forces server-side compression, optional arg: xz,9
|
||||||
|
|
||||||
|
\033[0mupload rules:
|
||||||
|
\033[36mmaxn=250,600\033[35m max 250 uploads over 15min
|
||||||
|
\033[36mmaxb=1g,300\033[35m max 1 GiB over 5min (suffixes: b, k, m, g)
|
||||||
|
\033[36msz=1k-3m\033[35m allow filesizes between 1 KiB and 3MiB
|
||||||
|
|
||||||
|
\033[0mupload rotation:
|
||||||
|
(moves all uploads into the specified folder structure)
|
||||||
|
\033[36mrotn=100,3\033[35m 3 levels of subfolders with 100 entries in each
|
||||||
|
\033[36mrotf=%Y-%m/%d-%H\033[35m date-formatted organizing
|
||||||
|
\033[36mlifetime=3600\033[35m uploads are deleted after 1 hour
|
||||||
|
|
||||||
|
\033[0mdatabase, general:
|
||||||
|
\033[36me2d\033[35m sets -e2d (all -e2* args can be set using ce2* volflags)
|
||||||
|
\033[36md2t\033[35m disables metadata collection, overrides -e2t*
|
||||||
|
\033[36md2d\033[35m disables all database stuff, overrides -e2*
|
||||||
|
\033[36mdhash\033[35m disables file hashing on initial scans, also ehash
|
||||||
|
\033[36mhist=/tmp/cdb\033[35m puts thumbnails and indexes at that location
|
||||||
|
\033[36mscan=60\033[35m scan for new files every 60sec, same as --re-maxage
|
||||||
|
|
||||||
|
\033[0mdatabase, audio tags:
|
||||||
|
"mte", "mth", "mtp", "mtm" all work the same as -mte, -mth, ...
|
||||||
|
\033[36mmtp=.bpm=f,audio-bpm.py\033[35m uses the "audio-bpm.py" program to
|
||||||
|
generate ".bpm" tags from uploads (f = overwrite tags)
|
||||||
|
\033[36mmtp=ahash,vhash=media-hash.py\033[35m collects two tags at once
|
||||||
|
\033[0m"""
|
||||||
|
),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"urlform",
|
||||||
|
"",
|
||||||
|
dedent(
|
||||||
|
"""
|
||||||
values for --urlform:
|
values for --urlform:
|
||||||
"stash" dumps the data to file and returns length + checksum
|
\033[36mstash\033[35m dumps the data to file and returns length + checksum
|
||||||
"save,get" dumps to file and returns the page like a GET
|
\033[36msave,get\033[35m dumps to file and returns the page like a GET
|
||||||
"print,get" prints the data in the log and returns GET
|
\033[36mprint,get\033[35m prints the data in the log and returns GET
|
||||||
(leave out the ",get" to return an error instead)
|
(leave out the ",get" to return an error instead)
|
||||||
|
"""
|
||||||
values for --ls:
|
),
|
||||||
"USR" is a user to browse as; * is anonymous, ** is all users
|
],
|
||||||
"VOL" is a single volume to scan, default is * (all vols)
|
[
|
||||||
"FLAG" is flags;
|
"ls",
|
||||||
"v" in addition to realpaths, print usernames and vpaths
|
"volume inspection",
|
||||||
"ln" only prints symlinks leaving the volume mountpoint
|
dedent(
|
||||||
"p" exits 1 if any such symlinks are found
|
"""
|
||||||
"r" resumes startup after the listing
|
\033[35m--ls USR,VOL,FLAGS
|
||||||
|
\033[36mUSR\033[0m is a user to browse as; * is anonymous, ** is all users
|
||||||
|
\033[36mVOL\033[0m is a single volume to scan, default is * (all vols)
|
||||||
|
\033[36mFLAG\033[0m is flags;
|
||||||
|
\033[36mv\033[0m in addition to realpaths, print usernames and vpaths
|
||||||
|
\033[36mln\033[0m only prints symlinks leaving the volume mountpoint
|
||||||
|
\033[36mp\033[0m exits 1 if any such symlinks are found
|
||||||
|
\033[36mr\033[0m resumes startup after the listing
|
||||||
examples:
|
examples:
|
||||||
--ls '**' # list all files which are possible to read
|
--ls '**' # list all files which are possible to read
|
||||||
--ls '**,*,ln' # check for dangerous symlinks
|
--ls '**,*,ln' # check for dangerous symlinks
|
||||||
--ls '**,*,ln,p,r' # check, then start normally if safe
|
--ls '**,*,ln,p,r' # check, then start normally if safe
|
||||||
\033[0m
|
|
||||||
"""
|
"""
|
||||||
),
|
),
|
||||||
)
|
],
|
||||||
|
]
|
||||||
|
|
||||||
# fmt: off
|
# fmt: off
|
||||||
|
u = unicode
|
||||||
ap2 = ap.add_argument_group('general options')
|
ap2 = ap.add_argument_group('general options')
|
||||||
ap2.add_argument("-c", metavar="PATH", type=str, action="append", help="add config file")
|
ap2.add_argument("-c", metavar="PATH", type=u, action="append", help="add config file")
|
||||||
ap2.add_argument("-nc", metavar="NUM", type=int, default=64, help="max num clients")
|
ap2.add_argument("-nc", metavar="NUM", type=int, default=64, help="max num clients")
|
||||||
ap2.add_argument("-j", metavar="CORES", type=int, default=1, help="max num cpu cores")
|
ap2.add_argument("-j", metavar="CORES", type=int, default=1, help="max num cpu cores")
|
||||||
ap2.add_argument("-a", metavar="ACCT", type=str, action="append", help="add account, USER:PASS; example [ed:wark")
|
ap2.add_argument("-a", metavar="ACCT", type=u, action="append", help="add account, USER:PASS; example [ed:wark")
|
||||||
ap2.add_argument("-v", metavar="VOL", type=str, action="append", help="add volume, SRC:DST:FLAG; example [.::r], [/mnt/nas/music:/music:r:aed")
|
ap2.add_argument("-v", metavar="VOL", type=u, action="append", help="add volume, SRC:DST:FLAG; example [.::r], [/mnt/nas/music:/music:r:aed")
|
||||||
ap2.add_argument("-ed", action="store_true", help="enable ?dots")
|
ap2.add_argument("-ed", action="store_true", help="enable ?dots")
|
||||||
ap2.add_argument("-emp", action="store_true", help="enable markdown plugins")
|
ap2.add_argument("-emp", action="store_true", help="enable markdown plugins")
|
||||||
ap2.add_argument("-mcr", metavar="SEC", type=int, default=60, help="md-editor mod-chk rate")
|
ap2.add_argument("-mcr", metavar="SEC", type=int, default=60, help="md-editor mod-chk rate")
|
||||||
|
ap2.add_argument("--urlform", metavar="MODE", type=u, default="print,get", help="how to handle url-forms; examples: [stash], [save,get]")
|
||||||
|
|
||||||
|
ap2 = ap.add_argument_group('upload options')
|
||||||
ap2.add_argument("--dotpart", action="store_true", help="dotfile incomplete uploads")
|
ap2.add_argument("--dotpart", action="store_true", help="dotfile incomplete uploads")
|
||||||
ap2.add_argument("--sparse", metavar="MiB", type=int, default=4, help="up2k min.size threshold (mswin-only)")
|
ap2.add_argument("--sparse", metavar="MiB", type=int, default=4, help="up2k min.size threshold (mswin-only)")
|
||||||
ap2.add_argument("--urlform", metavar="MODE", type=str, default="print,get", help="how to handle url-forms; examples: [stash], [save,get]")
|
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")
|
||||||
|
|
||||||
ap2 = ap.add_argument_group('network options')
|
ap2 = ap.add_argument_group('network options')
|
||||||
ap2.add_argument("-i", metavar="IP", type=str, default="0.0.0.0", help="ip to bind (comma-sep.)")
|
ap2.add_argument("-i", metavar="IP", type=u, default="0.0.0.0", help="ip to bind (comma-sep.)")
|
||||||
ap2.add_argument("-p", metavar="PORT", type=str, default="3923", help="ports to bind (comma/range)")
|
ap2.add_argument("-p", metavar="PORT", type=u, default="3923", help="ports to bind (comma/range)")
|
||||||
ap2.add_argument("--rproxy", metavar="DEPTH", type=int, default=1, help="which ip to keep; 0 = tcp, 1 = origin (first x-fwd), 2 = cloudflare, 3 = nginx, -1 = closest proxy")
|
ap2.add_argument("--rproxy", metavar="DEPTH", type=int, default=1, help="which ip to keep; 0 = tcp, 1 = origin (first x-fwd), 2 = cloudflare, 3 = nginx, -1 = closest proxy")
|
||||||
|
|
||||||
ap2 = ap.add_argument_group('SSL/TLS options')
|
ap2 = ap.add_argument_group('SSL/TLS options')
|
||||||
ap2.add_argument("--http-only", action="store_true", help="disable ssl/tls")
|
ap2.add_argument("--http-only", action="store_true", help="disable ssl/tls")
|
||||||
ap2.add_argument("--https-only", action="store_true", help="disable plaintext")
|
ap2.add_argument("--https-only", action="store_true", help="disable plaintext")
|
||||||
ap2.add_argument("--ssl-ver", metavar="LIST", type=str, help="set allowed ssl/tls versions; [help] shows available versions; default is what your python version considers safe")
|
ap2.add_argument("--ssl-ver", metavar="LIST", type=u, help="set allowed ssl/tls versions; [help] shows available versions; default is what your python version considers safe")
|
||||||
ap2.add_argument("--ciphers", metavar="LIST", help="set allowed ssl/tls ciphers; [help] shows available ciphers")
|
ap2.add_argument("--ciphers", metavar="LIST", type=u, help="set allowed ssl/tls ciphers; [help] shows available ciphers")
|
||||||
ap2.add_argument("--ssl-dbg", action="store_true", help="dump some tls info")
|
ap2.add_argument("--ssl-dbg", action="store_true", help="dump some tls info")
|
||||||
ap2.add_argument("--ssl-log", metavar="PATH", help="log master secrets")
|
ap2.add_argument("--ssl-log", metavar="PATH", type=u, help="log master secrets")
|
||||||
|
|
||||||
ap2 = ap.add_argument_group('opt-outs')
|
ap2 = ap.add_argument_group('opt-outs')
|
||||||
ap2.add_argument("-nw", action="store_true", help="disable writes (benchmark)")
|
ap2.add_argument("-nw", action="store_true", help="disable writes (benchmark)")
|
||||||
|
ap2.add_argument("--no-del", action="store_true", help="disable delete operations")
|
||||||
|
ap2.add_argument("--no-mv", action="store_true", help="disable move/rename operations")
|
||||||
ap2.add_argument("-nih", action="store_true", help="no info hostname")
|
ap2.add_argument("-nih", action="store_true", help="no info hostname")
|
||||||
ap2.add_argument("-nid", action="store_true", help="no info disk-usage")
|
ap2.add_argument("-nid", action="store_true", help="no info disk-usage")
|
||||||
ap2.add_argument("--no-zip", action="store_true", help="disable download as zip/tar")
|
ap2.add_argument("--no-zip", action="store_true", help="disable download as zip/tar")
|
||||||
|
ap2.add_argument("--no-lifetime", action="store_true", help="disable automatic deletion of uploads after a certain time (lifetime volflag)")
|
||||||
|
|
||||||
ap2 = ap.add_argument_group('safety options')
|
ap2 = ap.add_argument_group('safety options')
|
||||||
ap2.add_argument("--ls", metavar="U[,V[,F]]", help="scan all volumes; arguments USER,VOL,FLAGS; example [**,*,ln,p,r]")
|
ap2.add_argument("--ls", metavar="U[,V[,F]]", type=u, help="scan all volumes; arguments USER,VOL,FLAGS; example [**,*,ln,p,r]")
|
||||||
ap2.add_argument("--salt", type=str, default="hunter2", help="up2k file-hash salt")
|
ap2.add_argument("--salt", type=u, default="hunter2", help="up2k file-hash salt")
|
||||||
|
ap2.add_argument("--no-dot-mv", action="store_true", help="disallow moving dotfiles; makes it impossible to move folders containing dotfiles")
|
||||||
|
ap2.add_argument("--no-dot-ren", action="store_true", help="disallow renaming dotfiles; makes it impossible to make something a dotfile")
|
||||||
|
ap2.add_argument("--no-logues", action="store_true", help="disable rendering .prologue/.epilogue.html into directory listings")
|
||||||
|
ap2.add_argument("--no-readme", action="store_true", help="disable rendering readme.md into directory listings")
|
||||||
|
|
||||||
ap2 = ap.add_argument_group('logging options')
|
ap2 = ap.add_argument_group('logging options')
|
||||||
ap2.add_argument("-q", action="store_true", help="quiet")
|
ap2.add_argument("-q", action="store_true", help="quiet")
|
||||||
ap2.add_argument("-lo", metavar="PATH", type=str, help="logfile, example: cpp-%%Y-%%m%%d-%%H%%M%%S.txt.xz")
|
ap2.add_argument("-lo", metavar="PATH", type=u, help="logfile, example: cpp-%%Y-%%m%%d-%%H%%M%%S.txt.xz")
|
||||||
|
ap2.add_argument("--no-voldump", action="store_true", help="do not list volumes and permissions on startup")
|
||||||
ap2.add_argument("--log-conn", action="store_true", help="print tcp-server msgs")
|
ap2.add_argument("--log-conn", action="store_true", help="print tcp-server msgs")
|
||||||
ap2.add_argument("--log-htp", action="store_true", help="print http-server threadpool scaling")
|
ap2.add_argument("--log-htp", action="store_true", help="print http-server threadpool scaling")
|
||||||
ap2.add_argument("--ihead", metavar="HEADER", action='append', help="dump incoming header")
|
ap2.add_argument("--ihead", metavar="HEADER", type=u, action='append', help="dump incoming header")
|
||||||
ap2.add_argument("--lf-url", metavar="RE", type=str, default=r"^/\.cpr/|\?th=[wj]$", help="dont log URLs matching")
|
ap2.add_argument("--lf-url", metavar="RE", type=u, default=r"^/\.cpr/|\?th=[wj]$", help="dont log URLs matching")
|
||||||
|
|
||||||
ap2 = ap.add_argument_group('admin panel options')
|
ap2 = ap.add_argument_group('admin panel options')
|
||||||
ap2.add_argument("--no-rescan", action="store_true", help="disable ?scan (volume reindexing)")
|
ap2.add_argument("--no-rescan", action="store_true", help="disable ?scan (volume reindexing)")
|
||||||
@@ -310,45 +382,66 @@ def run_argparse(argv, formatter):
|
|||||||
ap2.add_argument("--no-thumb", action="store_true", help="disable all thumbnails")
|
ap2.add_argument("--no-thumb", action="store_true", help="disable all thumbnails")
|
||||||
ap2.add_argument("--no-vthumb", action="store_true", help="disable video thumbnails")
|
ap2.add_argument("--no-vthumb", action="store_true", help="disable video thumbnails")
|
||||||
ap2.add_argument("--th-size", metavar="WxH", default="320x256", help="thumbnail res")
|
ap2.add_argument("--th-size", metavar="WxH", default="320x256", help="thumbnail res")
|
||||||
|
ap2.add_argument("--th-mt", metavar="CORES", type=int, default=0, help="max num cpu cores to use, 0=all")
|
||||||
ap2.add_argument("--th-no-crop", action="store_true", help="dynamic height; show full image")
|
ap2.add_argument("--th-no-crop", action="store_true", help="dynamic height; show full image")
|
||||||
ap2.add_argument("--th-no-jpg", action="store_true", help="disable jpg output")
|
ap2.add_argument("--th-no-jpg", action="store_true", help="disable jpg output")
|
||||||
ap2.add_argument("--th-no-webp", action="store_true", help="disable webp output")
|
ap2.add_argument("--th-no-webp", action="store_true", help="disable webp output")
|
||||||
ap2.add_argument("--th-ff-jpg", action="store_true", help="force jpg for video thumbs")
|
ap2.add_argument("--th-ff-jpg", action="store_true", help="force jpg for video thumbs")
|
||||||
ap2.add_argument("--th-poke", metavar="SEC", type=int, default=300, help="activity labeling cooldown")
|
ap2.add_argument("--th-poke", metavar="SEC", type=int, default=300, help="activity labeling cooldown")
|
||||||
ap2.add_argument("--th-clean", metavar="SEC", type=int, default=43200, help="cleanup interval")
|
ap2.add_argument("--th-clean", metavar="SEC", type=int, default=43200, help="cleanup interval; 0=disabled")
|
||||||
ap2.add_argument("--th-maxage", metavar="SEC", type=int, default=604800, help="max folder age")
|
ap2.add_argument("--th-maxage", metavar="SEC", type=int, default=604800, help="max folder age")
|
||||||
ap2.add_argument("--th-covers", metavar="N,N", type=str, default="folder.png,folder.jpg,cover.png,cover.jpg", help="folder thumbnails to stat for")
|
ap2.add_argument("--th-covers", metavar="N,N", type=u, default="folder.png,folder.jpg,cover.png,cover.jpg", help="folder thumbnails to stat for")
|
||||||
|
|
||||||
ap2 = ap.add_argument_group('database options')
|
ap2 = ap.add_argument_group('general db options')
|
||||||
ap2.add_argument("-e2d", action="store_true", help="enable up2k database")
|
ap2.add_argument("-e2d", action="store_true", help="enable up2k database")
|
||||||
ap2.add_argument("-e2ds", action="store_true", help="enable up2k db-scanner, sets -e2d")
|
ap2.add_argument("-e2ds", action="store_true", help="enable up2k db-scanner, sets -e2d")
|
||||||
ap2.add_argument("-e2dsa", action="store_true", help="scan all folders (for search), sets -e2ds")
|
ap2.add_argument("-e2dsa", action="store_true", help="scan all folders (for search), sets -e2ds")
|
||||||
|
ap2.add_argument("--hist", metavar="PATH", type=u, help="where to store volume data (db, thumbs)")
|
||||||
|
ap2.add_argument("--no-hash", action="store_true", help="disable hashing during e2ds folder scans")
|
||||||
|
ap2.add_argument("--re-int", metavar="SEC", type=int, default=30, help="disk rescan check interval")
|
||||||
|
ap2.add_argument("--re-maxage", metavar="SEC", type=int, default=0, help="disk rescan volume interval, 0=off, can be set per-volume with the 'scan' volflag")
|
||||||
|
ap2.add_argument("--srch-time", metavar="SEC", type=int, default=30, help="search deadline")
|
||||||
|
|
||||||
|
ap2 = ap.add_argument_group('metadata db options')
|
||||||
ap2.add_argument("-e2t", action="store_true", help="enable metadata indexing")
|
ap2.add_argument("-e2t", action="store_true", help="enable metadata indexing")
|
||||||
ap2.add_argument("-e2ts", action="store_true", help="enable metadata scanner, sets -e2t")
|
ap2.add_argument("-e2ts", action="store_true", help="enable metadata scanner, sets -e2t")
|
||||||
ap2.add_argument("-e2tsr", action="store_true", help="rescan all metadata, sets -e2ts")
|
ap2.add_argument("-e2tsr", action="store_true", help="rescan all metadata, sets -e2ts")
|
||||||
ap2.add_argument("--hist", metavar="PATH", type=str, help="where to store volume state")
|
ap2.add_argument("--no-mutagen", action="store_true", help="use FFprobe for tags instead")
|
||||||
ap2.add_argument("--no-hash", action="store_true", help="disable hashing during e2ds folder scans")
|
|
||||||
ap2.add_argument("--no-mutagen", action="store_true", help="use ffprobe for tags instead")
|
|
||||||
ap2.add_argument("--no-mtag-mt", action="store_true", help="disable tag-read parallelism")
|
ap2.add_argument("--no-mtag-mt", action="store_true", help="disable tag-read parallelism")
|
||||||
ap2.add_argument("-mtm", metavar="M=t,t,t", action="append", type=str, help="add/replace metadata mapping")
|
ap2.add_argument("--no-mtag-ff", action="store_true", help="never use FFprobe as tag reader")
|
||||||
ap2.add_argument("-mte", metavar="M,M,M", type=str, help="tags to index/display (comma-sep.)",
|
ap2.add_argument("-mtm", metavar="M=t,t,t", type=u, action="append", help="add/replace metadata mapping")
|
||||||
default="circle,album,.tn,artist,title,.bpm,key,.dur,.q,.vq,.aq,ac,vc,res,.fps")
|
ap2.add_argument("-mte", metavar="M,M,M", type=u, help="tags to index/display (comma-sep.)",
|
||||||
ap2.add_argument("-mtp", metavar="M=[f,]bin", action="append", type=str, help="read tag M using bin")
|
default="circle,album,.tn,artist,title,.bpm,key,.dur,.q,.vq,.aq,vc,ac,res,.fps,ahash,vhash")
|
||||||
ap2.add_argument("--srch-time", metavar="SEC", type=int, default=30, help="search deadline")
|
ap2.add_argument("-mth", metavar="M,M,M", type=u, help="tags to hide by default (comma-sep.)",
|
||||||
|
default=".vq,.aq,vc,ac,res,.fps")
|
||||||
|
ap2.add_argument("-mtp", metavar="M=[f,]bin", type=u, action="append", help="read tag M using bin")
|
||||||
|
|
||||||
ap2 = ap.add_argument_group('appearance options')
|
ap2 = ap.add_argument_group('appearance options')
|
||||||
ap2.add_argument("--css-browser", metavar="L", help="URL to additional CSS to include")
|
ap2.add_argument("--css-browser", metavar="L", type=u, help="URL to additional CSS to include")
|
||||||
|
|
||||||
ap2 = ap.add_argument_group('debug options')
|
ap2 = ap.add_argument_group('debug options')
|
||||||
ap2.add_argument("--no-sendfile", action="store_true", help="disable sendfile")
|
ap2.add_argument("--no-sendfile", action="store_true", help="disable sendfile")
|
||||||
ap2.add_argument("--no-scandir", action="store_true", help="disable scandir")
|
ap2.add_argument("--no-scandir", action="store_true", help="disable scandir")
|
||||||
ap2.add_argument("--no-fastboot", action="store_true", help="wait for up2k indexing")
|
ap2.add_argument("--no-fastboot", action="store_true", help="wait for up2k indexing")
|
||||||
ap2.add_argument("--no-htp", action="store_true", help="disable httpserver threadpool, create threads as-needed instead")
|
ap2.add_argument("--no-htp", action="store_true", help="disable httpserver threadpool, create threads as-needed instead")
|
||||||
ap2.add_argument("--stackmon", metavar="P,S", help="write stacktrace to Path every S second")
|
ap2.add_argument("--stackmon", metavar="P,S", type=u, help="write stacktrace to Path every S second")
|
||||||
|
ap2.add_argument("--log-thrs", metavar="SEC", type=float, help="list active threads every SEC")
|
||||||
return ap.parse_args(args=argv[1:])
|
|
||||||
# fmt: on
|
# fmt: on
|
||||||
|
|
||||||
|
ap2 = ap.add_argument_group("help sections")
|
||||||
|
for k, h, _ in sects:
|
||||||
|
ap2.add_argument("--help-" + k, action="store_true", help=h)
|
||||||
|
|
||||||
|
ret = ap.parse_args(args=argv[1:])
|
||||||
|
for k, h, t in sects:
|
||||||
|
k2 = "help_" + k.replace("-", "_")
|
||||||
|
if vars(ret)[k2]:
|
||||||
|
lprint("# {} help page".format(k))
|
||||||
|
lprint(t + "\033[0m")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
def main(argv=None):
|
def main(argv=None):
|
||||||
time.strptime("19970815", "%Y%m%d") # python#7980
|
time.strptime("19970815", "%Y%m%d") # python#7980
|
||||||
@@ -384,15 +477,41 @@ def main(argv=None):
|
|||||||
except AssertionError:
|
except AssertionError:
|
||||||
al = run_argparse(argv, Dodge11874)
|
al = run_argparse(argv, Dodge11874)
|
||||||
|
|
||||||
if al.stackmon:
|
nstrs = []
|
||||||
fp, f = al.stackmon.rsplit(",", 1)
|
anymod = False
|
||||||
f = int(f)
|
for ostr in al.v or []:
|
||||||
t = threading.Thread(
|
m = re_vol.match(ostr)
|
||||||
target=stackmon,
|
if not m:
|
||||||
args=(fp, f),
|
# not our problem
|
||||||
)
|
nstrs.append(ostr)
|
||||||
t.daemon = True
|
continue
|
||||||
t.start()
|
|
||||||
|
src, dst, perms = m.groups()
|
||||||
|
na = [src, dst]
|
||||||
|
mod = False
|
||||||
|
for opt in perms.split(":"):
|
||||||
|
if re.match("c[^,]", opt):
|
||||||
|
mod = True
|
||||||
|
na.append("c," + opt[1:])
|
||||||
|
elif re.sub("^[rwmd]*", "", opt) and "," not in opt:
|
||||||
|
mod = True
|
||||||
|
perm = opt[0]
|
||||||
|
if perm == "a":
|
||||||
|
perm = "rw"
|
||||||
|
na.append(perm + "," + opt[1:])
|
||||||
|
else:
|
||||||
|
na.append(opt)
|
||||||
|
|
||||||
|
nstr = ":".join(na)
|
||||||
|
nstrs.append(nstr if mod else ostr)
|
||||||
|
if mod:
|
||||||
|
msg = "\033[1;31mWARNING:\033[0;1m\n -v {} \033[0;33mwas replaced with\033[0;1m\n -v {} \n\033[0m"
|
||||||
|
lprint(msg.format(ostr, nstr))
|
||||||
|
anymod = True
|
||||||
|
|
||||||
|
if anymod:
|
||||||
|
al.v = nstrs
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
# propagate implications
|
# propagate implications
|
||||||
for k1, k2 in IMPLICATIONS:
|
for k1, k2 in IMPLICATIONS:
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
|
|
||||||
VERSION = (0, 11, 34)
|
VERSION = (1, 0, 0)
|
||||||
CODENAME = "the grid"
|
CODENAME = "sufficient"
|
||||||
BUILD_DT = (2021, 7, 9)
|
BUILD_DT = (2021, 9, 7)
|
||||||
|
|
||||||
S_VERSION = ".".join(map(str, VERSION))
|
S_VERSION = ".".join(map(str, VERSION))
|
||||||
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)
|
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)
|
||||||
|
|||||||
@@ -5,41 +5,229 @@ import re
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import stat
|
import stat
|
||||||
|
import time
|
||||||
import base64
|
import base64
|
||||||
import hashlib
|
import hashlib
|
||||||
import threading
|
import threading
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from .__init__ import WINDOWS
|
from .__init__ import WINDOWS
|
||||||
from .util import IMPLICATIONS, uncyg, undot, Pebkac, fsdec, fsenc, statdir
|
from .util import (
|
||||||
|
IMPLICATIONS,
|
||||||
|
uncyg,
|
||||||
|
undot,
|
||||||
|
unhumanize,
|
||||||
|
absreal,
|
||||||
|
Pebkac,
|
||||||
|
fsenc,
|
||||||
|
statdir,
|
||||||
|
)
|
||||||
|
from .bos import bos
|
||||||
|
|
||||||
|
|
||||||
|
LEELOO_DALLAS = "leeloo_dallas"
|
||||||
|
|
||||||
|
|
||||||
|
class AXS(object):
|
||||||
|
def __init__(self, uread=None, uwrite=None, umove=None, udel=None):
|
||||||
|
self.uread = {} if uread is None else {k: 1 for k in uread}
|
||||||
|
self.uwrite = {} if uwrite is None else {k: 1 for k in uwrite}
|
||||||
|
self.umove = {} if umove is None else {k: 1 for k in umove}
|
||||||
|
self.udel = {} if udel is None else {k: 1 for k in udel}
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "AXS({})".format(
|
||||||
|
", ".join(
|
||||||
|
"{}={!r}".format(k, self.__dict__[k])
|
||||||
|
for k in "uread uwrite umove udel".split()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Lim(object):
|
||||||
|
def __init__(self):
|
||||||
|
self.nups = {} # num tracker
|
||||||
|
self.bups = {} # byte tracker list
|
||||||
|
self.bupc = {} # byte tracker cache
|
||||||
|
|
||||||
|
self.nosub = False # disallow subdirectories
|
||||||
|
|
||||||
|
self.smin = None # filesize min
|
||||||
|
self.smax = None # filesize max
|
||||||
|
|
||||||
|
self.bwin = None # bytes window
|
||||||
|
self.bmax = None # bytes max
|
||||||
|
self.nwin = None # num window
|
||||||
|
self.nmax = None # num max
|
||||||
|
|
||||||
|
self.rotn = None # rot num files
|
||||||
|
self.rotl = None # rot depth
|
||||||
|
self.rotf = None # rot datefmt
|
||||||
|
self.rot_re = None # rotf check
|
||||||
|
|
||||||
|
def set_rotf(self, fmt):
|
||||||
|
self.rotf = fmt
|
||||||
|
r = re.escape(fmt).replace("%Y", "[0-9]{4}").replace("%j", "[0-9]{3}")
|
||||||
|
r = re.sub("%[mdHMSWU]", "[0-9]{2}", r)
|
||||||
|
self.rot_re = re.compile("(^|/)" + r + "$")
|
||||||
|
|
||||||
|
def all(self, ip, rem, sz, abspath):
|
||||||
|
self.chk_nup(ip)
|
||||||
|
self.chk_bup(ip)
|
||||||
|
self.chk_rem(rem)
|
||||||
|
if sz != -1:
|
||||||
|
self.chk_sz(sz)
|
||||||
|
|
||||||
|
ap2, vp2 = self.rot(abspath)
|
||||||
|
if abspath == ap2:
|
||||||
|
return ap2, rem
|
||||||
|
|
||||||
|
return ap2, ("{}/{}".format(rem, vp2) if rem else vp2)
|
||||||
|
|
||||||
|
def chk_sz(self, sz):
|
||||||
|
if self.smin is not None and sz < self.smin:
|
||||||
|
raise Pebkac(400, "file too small")
|
||||||
|
|
||||||
|
if self.smax is not None and sz > self.smax:
|
||||||
|
raise Pebkac(400, "file too big")
|
||||||
|
|
||||||
|
def chk_rem(self, rem):
|
||||||
|
if self.nosub and rem:
|
||||||
|
raise Pebkac(500, "no subdirectories allowed")
|
||||||
|
|
||||||
|
def rot(self, path):
|
||||||
|
if not self.rotf and not self.rotn:
|
||||||
|
return path, ""
|
||||||
|
|
||||||
|
if self.rotf:
|
||||||
|
path = path.rstrip("/\\")
|
||||||
|
if self.rot_re.search(path.replace("\\", "/")):
|
||||||
|
return path, ""
|
||||||
|
|
||||||
|
suf = datetime.utcnow().strftime(self.rotf)
|
||||||
|
if path:
|
||||||
|
path += "/"
|
||||||
|
|
||||||
|
return path + suf, suf
|
||||||
|
|
||||||
|
ret = self.dive(path, self.rotl)
|
||||||
|
if not ret:
|
||||||
|
raise Pebkac(500, "no available slots in volume")
|
||||||
|
|
||||||
|
d = ret[len(path) :].strip("/\\").replace("\\", "/")
|
||||||
|
return ret, d
|
||||||
|
|
||||||
|
def dive(self, path, lvs):
|
||||||
|
items = bos.listdir(path)
|
||||||
|
|
||||||
|
if not lvs:
|
||||||
|
# at leaf level
|
||||||
|
return None if len(items) >= self.rotn else ""
|
||||||
|
|
||||||
|
dirs = [int(x) for x in items if x and all(y in "1234567890" for y in x)]
|
||||||
|
dirs.sort()
|
||||||
|
|
||||||
|
if not dirs:
|
||||||
|
# no branches yet; make one
|
||||||
|
sub = os.path.join(path, "0")
|
||||||
|
bos.mkdir(sub)
|
||||||
|
else:
|
||||||
|
# try newest branch only
|
||||||
|
sub = os.path.join(path, str(dirs[-1]))
|
||||||
|
|
||||||
|
ret = self.dive(sub, lvs - 1)
|
||||||
|
if ret is not None:
|
||||||
|
return os.path.join(sub, ret)
|
||||||
|
|
||||||
|
if len(dirs) >= self.rotn:
|
||||||
|
# full branch or root
|
||||||
|
return None
|
||||||
|
|
||||||
|
# make a branch
|
||||||
|
sub = os.path.join(path, str(dirs[-1] + 1))
|
||||||
|
bos.mkdir(sub)
|
||||||
|
ret = self.dive(sub, lvs - 1)
|
||||||
|
if ret is None:
|
||||||
|
raise Pebkac(500, "rotation bug")
|
||||||
|
|
||||||
|
return os.path.join(sub, ret)
|
||||||
|
|
||||||
|
def nup(self, ip):
|
||||||
|
try:
|
||||||
|
self.nups[ip].append(time.time())
|
||||||
|
except:
|
||||||
|
self.nups[ip] = [time.time()]
|
||||||
|
|
||||||
|
def bup(self, ip, nbytes):
|
||||||
|
v = [time.time(), nbytes]
|
||||||
|
try:
|
||||||
|
self.bups[ip].append(v)
|
||||||
|
self.bupc[ip] += nbytes
|
||||||
|
except:
|
||||||
|
self.bups[ip] = [v]
|
||||||
|
self.bupc[ip] = nbytes
|
||||||
|
|
||||||
|
def chk_nup(self, ip):
|
||||||
|
if not self.nmax or ip not in self.nups:
|
||||||
|
return
|
||||||
|
|
||||||
|
nups = self.nups[ip]
|
||||||
|
cutoff = time.time() - self.nwin
|
||||||
|
while nups and nups[0] < cutoff:
|
||||||
|
nups.pop(0)
|
||||||
|
|
||||||
|
if len(nups) >= self.nmax:
|
||||||
|
raise Pebkac(429, "too many uploads")
|
||||||
|
|
||||||
|
def chk_bup(self, ip):
|
||||||
|
if not self.bmax or ip not in self.bups:
|
||||||
|
return
|
||||||
|
|
||||||
|
bups = self.bups[ip]
|
||||||
|
cutoff = time.time() - self.bwin
|
||||||
|
mark = self.bupc[ip]
|
||||||
|
while bups and bups[0][0] < cutoff:
|
||||||
|
mark -= bups.pop(0)[1]
|
||||||
|
|
||||||
|
self.bupc[ip] = mark
|
||||||
|
if mark >= self.bmax:
|
||||||
|
raise Pebkac(429, "ingress saturated")
|
||||||
|
|
||||||
|
|
||||||
class VFS(object):
|
class VFS(object):
|
||||||
"""single level in the virtual fs"""
|
"""single level in the virtual fs"""
|
||||||
|
|
||||||
def __init__(self, log, realpath, vpath, uread=[], uwrite=[], uadm=[], flags={}):
|
def __init__(self, log, realpath, vpath, axs, flags):
|
||||||
self.log = log
|
self.log = log
|
||||||
self.realpath = realpath # absolute path on host filesystem
|
self.realpath = realpath # absolute path on host filesystem
|
||||||
self.vpath = vpath # absolute path in the virtual filesystem
|
self.vpath = vpath # absolute path in the virtual filesystem
|
||||||
self.uread = uread # users who can read this
|
self.axs = axs # type: AXS
|
||||||
self.uwrite = uwrite # users who can write this
|
self.flags = flags # config options
|
||||||
self.uadm = uadm # users who are regular admins
|
|
||||||
self.flags = flags # config switches
|
|
||||||
self.nodes = {} # child nodes
|
self.nodes = {} # child nodes
|
||||||
self.histtab = None # all realpath->histpath
|
self.histtab = None # all realpath->histpath
|
||||||
self.dbv = None # closest full/non-jump parent
|
self.dbv = None # closest full/non-jump parent
|
||||||
|
self.lim = None # type: Lim # upload limits; only set for dbv
|
||||||
|
|
||||||
if realpath:
|
if realpath:
|
||||||
self.histpath = os.path.join(realpath, ".hist") # db / thumbcache
|
self.histpath = os.path.join(realpath, ".hist") # db / thumbcache
|
||||||
self.all_vols = {vpath: self} # flattened recursive
|
self.all_vols = {vpath: self} # flattened recursive
|
||||||
|
self.aread = {}
|
||||||
|
self.awrite = {}
|
||||||
|
self.amove = {}
|
||||||
|
self.adel = {}
|
||||||
else:
|
else:
|
||||||
self.histpath = None
|
self.histpath = None
|
||||||
self.all_vols = None
|
self.all_vols = None
|
||||||
|
self.aread = None
|
||||||
|
self.awrite = None
|
||||||
|
self.amove = None
|
||||||
|
self.adel = None
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "VFS({})".format(
|
return "VFS({})".format(
|
||||||
", ".join(
|
", ".join(
|
||||||
"{}={!r}".format(k, self.__dict__[k])
|
"{}={!r}".format(k, self.__dict__[k])
|
||||||
for k in "realpath vpath uread uwrite uadm flags".split()
|
for k in "realpath vpath axs flags".split()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -66,9 +254,7 @@ class VFS(object):
|
|||||||
self.log,
|
self.log,
|
||||||
os.path.join(self.realpath, name) if self.realpath else None,
|
os.path.join(self.realpath, name) if self.realpath else None,
|
||||||
"{}/{}".format(self.vpath, name).lstrip("/"),
|
"{}/{}".format(self.vpath, name).lstrip("/"),
|
||||||
self.uread,
|
self.axs,
|
||||||
self.uwrite,
|
|
||||||
self.uadm,
|
|
||||||
self._copy_flags(name),
|
self._copy_flags(name),
|
||||||
)
|
)
|
||||||
vn.dbv = self.dbv or self
|
vn.dbv = self.dbv or self
|
||||||
@@ -81,7 +267,7 @@ class VFS(object):
|
|||||||
|
|
||||||
# leaf does not exist; create and keep permissions blank
|
# leaf does not exist; create and keep permissions blank
|
||||||
vp = "{}/{}".format(self.vpath, dst).lstrip("/")
|
vp = "{}/{}".format(self.vpath, dst).lstrip("/")
|
||||||
vn = VFS(self.log, src, vp)
|
vn = VFS(self.log, src, vp, AXS(), {})
|
||||||
vn.dbv = self.dbv or self
|
vn.dbv = self.dbv or self
|
||||||
self.nodes[dst] = vn
|
self.nodes[dst] = vn
|
||||||
return vn
|
return vn
|
||||||
@@ -121,27 +307,37 @@ class VFS(object):
|
|||||||
return [self, vpath]
|
return [self, vpath]
|
||||||
|
|
||||||
def can_access(self, vpath, uname):
|
def can_access(self, vpath, uname):
|
||||||
"""return [readable,writable]"""
|
# type: (str, str) -> tuple[bool, bool, bool, bool]
|
||||||
|
"""can Read,Write,Move,Delete"""
|
||||||
vn, _ = self._find(vpath)
|
vn, _ = self._find(vpath)
|
||||||
|
c = vn.axs
|
||||||
return [
|
return [
|
||||||
uname in vn.uread or "*" in vn.uread,
|
uname in c.uread or "*" in c.uread,
|
||||||
uname in vn.uwrite or "*" in vn.uwrite,
|
uname in c.uwrite or "*" in c.uwrite,
|
||||||
|
uname in c.umove or "*" in c.umove,
|
||||||
|
uname in c.udel or "*" in c.udel,
|
||||||
]
|
]
|
||||||
|
|
||||||
def get(self, vpath, uname, will_read, will_write):
|
def get(self, vpath, uname, will_read, will_write, will_move=False, will_del=False):
|
||||||
# type: (str, str, bool, bool) -> tuple[VFS, str]
|
# type: (str, str, bool, bool, bool, bool) -> tuple[VFS, str]
|
||||||
"""returns [vfsnode,fs_remainder] if user has the requested permissions"""
|
"""returns [vfsnode,fs_remainder] if user has the requested permissions"""
|
||||||
vn, rem = self._find(vpath)
|
vn, rem = self._find(vpath)
|
||||||
|
c = vn.axs
|
||||||
|
|
||||||
if will_read and (uname not in vn.uread and "*" not in vn.uread):
|
for req, d, msg in [
|
||||||
raise Pebkac(403, "you don't have read-access for this location")
|
[will_read, c.uread, "read"],
|
||||||
|
[will_write, c.uwrite, "write"],
|
||||||
if will_write and (uname not in vn.uwrite and "*" not in vn.uwrite):
|
[will_move, c.umove, "move"],
|
||||||
raise Pebkac(403, "you don't have write-access for this location")
|
[will_del, c.udel, "delete"],
|
||||||
|
]:
|
||||||
|
if req and (uname not in d and "*" not in d) and uname != LEELOO_DALLAS:
|
||||||
|
m = "you don't have {}-access for this location"
|
||||||
|
raise Pebkac(403, m.format(msg))
|
||||||
|
|
||||||
return vn, rem
|
return vn, rem
|
||||||
|
|
||||||
def get_dbv(self, vrem):
|
def get_dbv(self, vrem):
|
||||||
|
# type: (str) -> tuple[VFS, str]
|
||||||
dbv = self.dbv
|
dbv = self.dbv
|
||||||
if not dbv:
|
if not dbv:
|
||||||
return self, vrem
|
return self, vrem
|
||||||
@@ -150,65 +346,50 @@ class VFS(object):
|
|||||||
vrem = "/".join([x for x in vrem if x])
|
vrem = "/".join([x for x in vrem if x])
|
||||||
return dbv, vrem
|
return dbv, vrem
|
||||||
|
|
||||||
def canonical(self, rem):
|
def canonical(self, rem, resolve=True):
|
||||||
"""returns the canonical path (fully-resolved absolute fs path)"""
|
"""returns the canonical path (fully-resolved absolute fs path)"""
|
||||||
rp = self.realpath
|
rp = self.realpath
|
||||||
if rem:
|
if rem:
|
||||||
rp += "/" + rem
|
rp += "/" + rem
|
||||||
|
|
||||||
try:
|
return absreal(rp) if resolve else rp
|
||||||
return fsdec(os.path.realpath(fsenc(rp)))
|
|
||||||
except:
|
|
||||||
if not WINDOWS:
|
|
||||||
raise
|
|
||||||
|
|
||||||
# cpython bug introduced in 3.8, still exists in 3.9.1;
|
def ls(self, rem, uname, scandir, permsets, lstat=False):
|
||||||
# some win7sp1 and win10:20H2 boxes cannot realpath a
|
# type: (str, str, bool, list[list[bool]], bool) -> tuple[str, str, dict[str, VFS]]
|
||||||
# networked drive letter such as b"n:" or b"n:\\"
|
|
||||||
#
|
|
||||||
# requirements to trigger:
|
|
||||||
# * bytestring (not unicode str)
|
|
||||||
# * just the drive letter (subfolders are ok)
|
|
||||||
# * networked drive (regular disks and vmhgfs are ok)
|
|
||||||
# * on an enterprise network (idk, cannot repro with samba)
|
|
||||||
#
|
|
||||||
# hits the following exceptions in succession:
|
|
||||||
# * access denied at L601: "path = _getfinalpathname(path)"
|
|
||||||
# * "cant concat str to bytes" at L621: "return path + tail"
|
|
||||||
#
|
|
||||||
return os.path.realpath(rp)
|
|
||||||
|
|
||||||
def ls(self, rem, uname, scandir, incl_wo=False, lstat=False):
|
|
||||||
# type: (str, str, bool, bool, bool) -> tuple[str, str, dict[str, VFS]]
|
|
||||||
"""return user-readable [fsdir,real,virt] items at vpath"""
|
"""return user-readable [fsdir,real,virt] items at vpath"""
|
||||||
virt_vis = {} # nodes readable by user
|
virt_vis = {} # nodes readable by user
|
||||||
abspath = self.canonical(rem)
|
abspath = self.canonical(rem)
|
||||||
real = list(statdir(self.log, scandir, lstat, abspath))
|
real = list(statdir(self.log, scandir, lstat, abspath))
|
||||||
real.sort()
|
real.sort()
|
||||||
if not rem:
|
if not rem:
|
||||||
for name, vn2 in sorted(self.nodes.items()):
|
# no vfs nodes in the list of real inodes
|
||||||
ok = uname in vn2.uread or "*" in vn2.uread
|
real = [x for x in real if x[0] not in self.nodes]
|
||||||
|
|
||||||
if not ok and incl_wo:
|
for name, vn2 in sorted(self.nodes.items()):
|
||||||
ok = uname in vn2.uwrite or "*" in vn2.uwrite
|
ok = False
|
||||||
|
axs = vn2.axs
|
||||||
|
axs = [axs.uread, axs.uwrite, axs.umove, axs.udel]
|
||||||
|
for pset in permsets:
|
||||||
|
ok = True
|
||||||
|
for req, lst in zip(pset, axs):
|
||||||
|
if req and uname not in lst and "*" not in lst:
|
||||||
|
ok = False
|
||||||
|
if ok:
|
||||||
|
break
|
||||||
|
|
||||||
if ok:
|
if ok:
|
||||||
virt_vis[name] = vn2
|
virt_vis[name] = vn2
|
||||||
|
|
||||||
# no vfs nodes in the list of real inodes
|
|
||||||
real = [x for x in real if x[0] not in self.nodes]
|
|
||||||
|
|
||||||
return [abspath, real, virt_vis]
|
return [abspath, real, virt_vis]
|
||||||
|
|
||||||
def walk(self, rel, rem, seen, uname, dots, scandir, lstat):
|
def walk(self, rel, rem, seen, uname, permsets, dots, scandir, lstat):
|
||||||
"""
|
"""
|
||||||
recursively yields from ./rem;
|
recursively yields from ./rem;
|
||||||
rel is a unix-style user-defined vpath (not vfs-related)
|
rel is a unix-style user-defined vpath (not vfs-related)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
fsroot, vfs_ls, vfs_virt = self.ls(
|
fsroot, vfs_ls, vfs_virt = self.ls(rem, uname, scandir, permsets, lstat=lstat)
|
||||||
rem, uname, scandir, incl_wo=False, lstat=lstat
|
dbv, vrem = self.get_dbv(rem)
|
||||||
)
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
seen
|
seen
|
||||||
@@ -226,7 +407,7 @@ class VFS(object):
|
|||||||
rfiles.sort()
|
rfiles.sort()
|
||||||
rdirs.sort()
|
rdirs.sort()
|
||||||
|
|
||||||
yield rel, fsroot, rfiles, rdirs, vfs_virt
|
yield dbv, vrem, rel, fsroot, rfiles, rdirs, vfs_virt
|
||||||
|
|
||||||
for rdir, _ in rdirs:
|
for rdir, _ in rdirs:
|
||||||
if not dots and rdir.startswith("."):
|
if not dots and rdir.startswith("."):
|
||||||
@@ -234,7 +415,7 @@ class VFS(object):
|
|||||||
|
|
||||||
wrel = (rel + "/" + rdir).lstrip("/")
|
wrel = (rel + "/" + rdir).lstrip("/")
|
||||||
wrem = (rem + "/" + rdir).lstrip("/")
|
wrem = (rem + "/" + rdir).lstrip("/")
|
||||||
for x in self.walk(wrel, wrem, seen, uname, dots, scandir, lstat):
|
for x in self.walk(wrel, wrem, seen, uname, permsets, dots, scandir, lstat):
|
||||||
yield x
|
yield x
|
||||||
|
|
||||||
for n, vfs in sorted(vfs_virt.items()):
|
for n, vfs in sorted(vfs_virt.items()):
|
||||||
@@ -242,7 +423,7 @@ class VFS(object):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
wrel = (rel + "/" + n).lstrip("/")
|
wrel = (rel + "/" + n).lstrip("/")
|
||||||
for x in vfs.walk(wrel, "", seen, uname, dots, scandir, lstat):
|
for x in vfs.walk(wrel, "", seen, uname, permsets, dots, scandir, lstat):
|
||||||
yield x
|
yield x
|
||||||
|
|
||||||
def zipgen(self, vrem, flt, uname, dots, scandir):
|
def zipgen(self, vrem, flt, uname, dots, scandir):
|
||||||
@@ -253,9 +434,12 @@ class VFS(object):
|
|||||||
f2a = os.sep + "dir.txt"
|
f2a = os.sep + "dir.txt"
|
||||||
f2b = "{0}.hist{0}".format(os.sep)
|
f2b = "{0}.hist{0}".format(os.sep)
|
||||||
|
|
||||||
for vpath, apath, files, rd, vd in self.walk(
|
# if multiselect: add all items to archive root
|
||||||
"", vrem, [], uname, dots, scandir, False
|
# if single folder: the folder itself is the top-level item
|
||||||
):
|
folder = "" if flt else (vrem.split("/")[-1] or "top")
|
||||||
|
|
||||||
|
g = self.walk(folder, vrem, [], uname, [[True]], dots, scandir, False)
|
||||||
|
for _, _, vpath, apath, files, rd, vd in g:
|
||||||
if flt:
|
if flt:
|
||||||
files = [x for x in files if x[0] in flt]
|
files = [x for x in files if x[0] in flt]
|
||||||
|
|
||||||
@@ -295,19 +479,11 @@ class VFS(object):
|
|||||||
for f in [{"vp": v, "ap": a, "st": n[1]} for v, a, n in files]:
|
for f in [{"vp": v, "ap": a, "st": n[1]} for v, a, n in files]:
|
||||||
yield f
|
yield f
|
||||||
|
|
||||||
def user_tree(self, uname, readable, writable, admin):
|
|
||||||
is_readable = False
|
|
||||||
if uname in self.uread or "*" in self.uread:
|
|
||||||
readable.append(self.vpath)
|
|
||||||
is_readable = True
|
|
||||||
|
|
||||||
if uname in self.uwrite or "*" in self.uwrite:
|
if WINDOWS:
|
||||||
writable.append(self.vpath)
|
re_vol = re.compile(r"^([a-zA-Z]:[\\/][^:]*|[^:]*):([^:]*):(.*)$")
|
||||||
if is_readable:
|
else:
|
||||||
admin.append(self.vpath)
|
re_vol = re.compile(r"^([^:]*):([^:]*):(.*)$")
|
||||||
|
|
||||||
for _, vn in sorted(self.nodes.items()):
|
|
||||||
vn.user_tree(uname, readable, writable, admin)
|
|
||||||
|
|
||||||
|
|
||||||
class AuthSrv(object):
|
class AuthSrv(object):
|
||||||
@@ -319,11 +495,6 @@ class AuthSrv(object):
|
|||||||
self.warn_anonwrite = warn_anonwrite
|
self.warn_anonwrite = warn_anonwrite
|
||||||
self.line_ctr = 0
|
self.line_ctr = 0
|
||||||
|
|
||||||
if WINDOWS:
|
|
||||||
self.re_vol = re.compile(r"^([a-zA-Z]:[\\/][^:]*|[^:]*):([^:]*):(.*)$")
|
|
||||||
else:
|
|
||||||
self.re_vol = re.compile(r"^([^:]*):([^:]*):(.*)$")
|
|
||||||
|
|
||||||
self.mutex = threading.Lock()
|
self.mutex = threading.Lock()
|
||||||
self.reload()
|
self.reload()
|
||||||
|
|
||||||
@@ -341,7 +512,8 @@ class AuthSrv(object):
|
|||||||
|
|
||||||
yield prev, True
|
yield prev, True
|
||||||
|
|
||||||
def _parse_config_file(self, fd, user, mread, mwrite, madm, mflags, mount):
|
def _parse_config_file(self, fd, acct, daxs, mflags, mount):
|
||||||
|
# type: (any, str, dict[str, AXS], any, str) -> None
|
||||||
vol_src = None
|
vol_src = None
|
||||||
vol_dst = None
|
vol_dst = None
|
||||||
self.line_ctr = 0
|
self.line_ctr = 0
|
||||||
@@ -357,7 +529,7 @@ class AuthSrv(object):
|
|||||||
if vol_src is None:
|
if vol_src is None:
|
||||||
if ln.startswith("u "):
|
if ln.startswith("u "):
|
||||||
u, p = ln[2:].split(":", 1)
|
u, p = ln[2:].split(":", 1)
|
||||||
user[u] = p
|
acct[u] = p
|
||||||
else:
|
else:
|
||||||
vol_src = ln
|
vol_src = ln
|
||||||
continue
|
continue
|
||||||
@@ -368,50 +540,53 @@ class AuthSrv(object):
|
|||||||
raise Exception('invalid mountpoint "{}"'.format(vol_dst))
|
raise Exception('invalid mountpoint "{}"'.format(vol_dst))
|
||||||
|
|
||||||
# cfg files override arguments and previous files
|
# cfg files override arguments and previous files
|
||||||
vol_src = fsdec(os.path.abspath(fsenc(vol_src)))
|
vol_src = bos.path.abspath(vol_src)
|
||||||
vol_dst = vol_dst.strip("/")
|
vol_dst = vol_dst.strip("/")
|
||||||
mount[vol_dst] = vol_src
|
mount[vol_dst] = vol_src
|
||||||
mread[vol_dst] = []
|
daxs[vol_dst] = AXS()
|
||||||
mwrite[vol_dst] = []
|
|
||||||
madm[vol_dst] = []
|
|
||||||
mflags[vol_dst] = {}
|
mflags[vol_dst] = {}
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if len(ln) > 1:
|
try:
|
||||||
lvl, uname = ln.split(" ")
|
lvl, uname = ln.split(" ", 1)
|
||||||
else:
|
except:
|
||||||
lvl = ln
|
lvl = ln
|
||||||
uname = "*"
|
uname = "*"
|
||||||
|
|
||||||
self._read_vol_str(
|
if lvl == "a":
|
||||||
lvl,
|
m = "WARNING (config-file): permission flag 'a' is deprecated; please use 'rw' instead"
|
||||||
uname,
|
self.log(m, 1)
|
||||||
mread[vol_dst],
|
|
||||||
mwrite[vol_dst],
|
self._read_vol_str(lvl, uname, daxs[vol_dst], mflags[vol_dst])
|
||||||
madm[vol_dst],
|
|
||||||
mflags[vol_dst],
|
def _read_vol_str(self, lvl, uname, axs, flags):
|
||||||
)
|
# type: (str, str, AXS, any) -> None
|
||||||
|
if lvl.strip("crwmd"):
|
||||||
|
raise Exception("invalid volume flag: {},{}".format(lvl, uname))
|
||||||
|
|
||||||
def _read_vol_str(self, lvl, uname, mr, mw, ma, mf):
|
|
||||||
if lvl == "c":
|
if lvl == "c":
|
||||||
cval = True
|
cval = True
|
||||||
if "=" in uname:
|
if "=" in uname:
|
||||||
uname, cval = uname.split("=", 1)
|
uname, cval = uname.split("=", 1)
|
||||||
|
|
||||||
self._read_volflag(mf, uname, cval, False)
|
self._read_volflag(flags, uname, cval, False)
|
||||||
return
|
return
|
||||||
|
|
||||||
if uname == "":
|
if uname == "":
|
||||||
uname = "*"
|
uname = "*"
|
||||||
|
|
||||||
if lvl in "ra":
|
for un in uname.split(","):
|
||||||
mr.append(uname)
|
if "r" in lvl:
|
||||||
|
axs.uread[un] = 1
|
||||||
|
|
||||||
if lvl in "wa":
|
if "w" in lvl:
|
||||||
mw.append(uname)
|
axs.uwrite[un] = 1
|
||||||
|
|
||||||
if lvl == "a":
|
if "m" in lvl:
|
||||||
ma.append(uname)
|
axs.umove[un] = 1
|
||||||
|
|
||||||
|
if "d" in lvl:
|
||||||
|
axs.udel[un] = 1
|
||||||
|
|
||||||
def _read_volflag(self, flags, name, value, is_list):
|
def _read_volflag(self, flags, name, value, is_list):
|
||||||
if name not in ["mtp"]:
|
if name not in ["mtp"]:
|
||||||
@@ -433,23 +608,26 @@ class AuthSrv(object):
|
|||||||
before finally building the VFS
|
before finally building the VFS
|
||||||
"""
|
"""
|
||||||
|
|
||||||
user = {} # username:password
|
acct = {} # username:password
|
||||||
mread = {} # mountpoint:[username]
|
daxs = {} # type: dict[str, AXS]
|
||||||
mwrite = {} # mountpoint:[username]
|
|
||||||
madm = {} # mountpoint:[username]
|
|
||||||
mflags = {} # mountpoint:[flag]
|
mflags = {} # mountpoint:[flag]
|
||||||
mount = {} # dst:src (mountpoint:realpath)
|
mount = {} # dst:src (mountpoint:realpath)
|
||||||
|
|
||||||
if self.args.a:
|
if self.args.a:
|
||||||
# list of username:password
|
# list of username:password
|
||||||
for u, p in [x.split(":", 1) for x in self.args.a]:
|
for x in self.args.a:
|
||||||
user[u] = p
|
try:
|
||||||
|
u, p = x.split(":", 1)
|
||||||
|
acct[u] = p
|
||||||
|
except:
|
||||||
|
m = '\n invalid value "{}" for argument -a, must be username:password'
|
||||||
|
raise Exception(m.format(x))
|
||||||
|
|
||||||
if self.args.v:
|
if self.args.v:
|
||||||
# list of src:dst:permset:permset:...
|
# list of src:dst:permset:permset:...
|
||||||
# permset is [rwa]username or [c]flag
|
# permset is <rwmd>[,username][,username] or <c>,<flag>[=args]
|
||||||
for v_str in self.args.v:
|
for v_str in self.args.v:
|
||||||
m = self.re_vol.match(v_str)
|
m = re_vol.match(v_str)
|
||||||
if not m:
|
if not m:
|
||||||
raise Exception("invalid -v argument: [{}]".format(v_str))
|
raise Exception("invalid -v argument: [{}]".format(v_str))
|
||||||
|
|
||||||
@@ -458,27 +636,21 @@ class AuthSrv(object):
|
|||||||
src = uncyg(src)
|
src = uncyg(src)
|
||||||
|
|
||||||
# print("\n".join([src, dst, perms]))
|
# print("\n".join([src, dst, perms]))
|
||||||
src = fsdec(os.path.abspath(fsenc(src)))
|
src = bos.path.abspath(src)
|
||||||
dst = dst.strip("/")
|
dst = dst.strip("/")
|
||||||
mount[dst] = src
|
mount[dst] = src
|
||||||
mread[dst] = []
|
daxs[dst] = AXS()
|
||||||
mwrite[dst] = []
|
|
||||||
madm[dst] = []
|
|
||||||
mflags[dst] = {}
|
mflags[dst] = {}
|
||||||
|
|
||||||
perms = perms.split(":")
|
for x in perms.split(":"):
|
||||||
for (lvl, uname) in [[x[0], x[1:]] for x in perms]:
|
lvl, uname = x.split(",", 1) if "," in x else [x, ""]
|
||||||
self._read_vol_str(
|
self._read_vol_str(lvl, uname, daxs[dst], mflags[dst])
|
||||||
lvl, uname, mread[dst], mwrite[dst], madm[dst], mflags[dst]
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.args.c:
|
if self.args.c:
|
||||||
for cfg_fn in self.args.c:
|
for cfg_fn in self.args.c:
|
||||||
with open(cfg_fn, "rb") as f:
|
with open(cfg_fn, "rb") as f:
|
||||||
try:
|
try:
|
||||||
self._parse_config_file(
|
self._parse_config_file(f, acct, daxs, mflags, mount)
|
||||||
f, user, mread, mwrite, madm, mflags, mount
|
|
||||||
)
|
|
||||||
except:
|
except:
|
||||||
m = "\n\033[1;31m\nerror in config file {} on line {}:\n\033[0m"
|
m = "\n\033[1;31m\nerror in config file {} on line {}:\n\033[0m"
|
||||||
self.log(m.format(cfg_fn, self.line_ctr), 1)
|
self.log(m.format(cfg_fn, self.line_ctr), 1)
|
||||||
@@ -488,19 +660,17 @@ class AuthSrv(object):
|
|||||||
if WINDOWS:
|
if WINDOWS:
|
||||||
cased = {}
|
cased = {}
|
||||||
for k, v in mount.items():
|
for k, v in mount.items():
|
||||||
try:
|
cased[k] = absreal(v)
|
||||||
cased[k] = fsdec(os.path.realpath(fsenc(v)))
|
|
||||||
except:
|
|
||||||
cased[k] = v
|
|
||||||
|
|
||||||
mount = cased
|
mount = cased
|
||||||
|
|
||||||
if not mount:
|
if not mount:
|
||||||
# -h says our defaults are CWD at root and read/write for everyone
|
# -h says our defaults are CWD at root and read/write for everyone
|
||||||
vfs = VFS(self.log_func, os.path.abspath("."), "", ["*"], ["*"])
|
axs = AXS(["*"], ["*"], None, None)
|
||||||
|
vfs = VFS(self.log_func, bos.path.abspath("."), "", axs, {})
|
||||||
elif "" not in mount:
|
elif "" not in mount:
|
||||||
# there's volumes but no root; make root inaccessible
|
# there's volumes but no root; make root inaccessible
|
||||||
vfs = VFS(self.log_func, None, "")
|
vfs = VFS(self.log_func, None, "", AXS(), {})
|
||||||
vfs.flags["d2d"] = True
|
vfs.flags["d2d"] = True
|
||||||
|
|
||||||
maxdepth = 0
|
maxdepth = 0
|
||||||
@@ -511,32 +681,34 @@ class AuthSrv(object):
|
|||||||
|
|
||||||
if dst == "":
|
if dst == "":
|
||||||
# rootfs was mapped; fully replaces the default CWD vfs
|
# rootfs was mapped; fully replaces the default CWD vfs
|
||||||
vfs = VFS(
|
vfs = VFS(self.log_func, mount[dst], dst, daxs[dst], mflags[dst])
|
||||||
self.log_func,
|
|
||||||
mount[dst],
|
|
||||||
dst,
|
|
||||||
mread[dst],
|
|
||||||
mwrite[dst],
|
|
||||||
madm[dst],
|
|
||||||
mflags[dst],
|
|
||||||
)
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
v = vfs.add(mount[dst], dst)
|
v = vfs.add(mount[dst], dst)
|
||||||
v.uread = mread[dst]
|
v.axs = daxs[dst]
|
||||||
v.uwrite = mwrite[dst]
|
|
||||||
v.uadm = madm[dst]
|
|
||||||
v.flags = mflags[dst]
|
v.flags = mflags[dst]
|
||||||
v.dbv = None
|
v.dbv = None
|
||||||
|
|
||||||
vfs.all_vols = {}
|
vfs.all_vols = {}
|
||||||
vfs.get_all_vols(vfs.all_vols)
|
vfs.get_all_vols(vfs.all_vols)
|
||||||
|
|
||||||
|
for perm in "read write move del".split():
|
||||||
|
axs_key = "u" + perm
|
||||||
|
unames = ["*"] + list(acct.keys())
|
||||||
|
umap = {x: [] for x in unames}
|
||||||
|
for usr in unames:
|
||||||
|
for mp, vol in vfs.all_vols.items():
|
||||||
|
if usr in getattr(vol.axs, axs_key):
|
||||||
|
umap[usr].append(mp)
|
||||||
|
setattr(vfs, "a" + perm, umap)
|
||||||
|
|
||||||
|
all_users = {}
|
||||||
missing_users = {}
|
missing_users = {}
|
||||||
for d in [mread, mwrite]:
|
for axs in daxs.values():
|
||||||
for _, ul in d.items():
|
for d in [axs.uread, axs.uwrite, axs.umove, axs.udel]:
|
||||||
for usr in ul:
|
for usr in d.keys():
|
||||||
if usr != "*" and usr not in user:
|
all_users[usr] = 1
|
||||||
|
if usr != "*" and usr not in acct:
|
||||||
missing_users[usr] = 1
|
missing_users[usr] = 1
|
||||||
|
|
||||||
if missing_users:
|
if missing_users:
|
||||||
@@ -547,6 +719,9 @@ class AuthSrv(object):
|
|||||||
)
|
)
|
||||||
raise Exception("invalid config")
|
raise Exception("invalid config")
|
||||||
|
|
||||||
|
if LEELOO_DALLAS in all_users:
|
||||||
|
raise Exception("sorry, reserved username: " + LEELOO_DALLAS)
|
||||||
|
|
||||||
promote = []
|
promote = []
|
||||||
demote = []
|
demote = []
|
||||||
for vol in vfs.all_vols.values():
|
for vol in vfs.all_vols.values():
|
||||||
@@ -560,10 +735,7 @@ class AuthSrv(object):
|
|||||||
elif self.args.hist:
|
elif self.args.hist:
|
||||||
for nch in range(len(hid)):
|
for nch in range(len(hid)):
|
||||||
hpath = os.path.join(self.args.hist, hid[: nch + 1])
|
hpath = os.path.join(self.args.hist, hid[: nch + 1])
|
||||||
try:
|
bos.makedirs(hpath)
|
||||||
os.makedirs(hpath)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
powner = os.path.join(hpath, "owner.txt")
|
powner = os.path.join(hpath, "owner.txt")
|
||||||
try:
|
try:
|
||||||
@@ -583,9 +755,9 @@ class AuthSrv(object):
|
|||||||
vol.histpath = hpath
|
vol.histpath = hpath
|
||||||
break
|
break
|
||||||
|
|
||||||
vol.histpath = os.path.realpath(vol.histpath)
|
vol.histpath = absreal(vol.histpath)
|
||||||
if vol.dbv:
|
if vol.dbv:
|
||||||
if os.path.exists(os.path.join(vol.histpath, "up2k.db")):
|
if bos.path.exists(os.path.join(vol.histpath, "up2k.db")):
|
||||||
promote.append(vol)
|
promote.append(vol)
|
||||||
vol.dbv = None
|
vol.dbv = None
|
||||||
else:
|
else:
|
||||||
@@ -608,10 +780,55 @@ class AuthSrv(object):
|
|||||||
|
|
||||||
vfs.histtab = {v.realpath: v.histpath for v in vfs.all_vols.values()}
|
vfs.histtab = {v.realpath: v.histpath for v in vfs.all_vols.values()}
|
||||||
|
|
||||||
|
for vol in vfs.all_vols.values():
|
||||||
|
lim = Lim()
|
||||||
|
use = False
|
||||||
|
|
||||||
|
if vol.flags.get("nosub"):
|
||||||
|
use = True
|
||||||
|
lim.nosub = True
|
||||||
|
|
||||||
|
v = vol.flags.get("sz")
|
||||||
|
if v:
|
||||||
|
use = True
|
||||||
|
lim.smin, lim.smax = [unhumanize(x) for x in v.split("-")]
|
||||||
|
|
||||||
|
v = vol.flags.get("rotn")
|
||||||
|
if v:
|
||||||
|
use = True
|
||||||
|
lim.rotn, lim.rotl = [int(x) for x in v.split(",")]
|
||||||
|
|
||||||
|
v = vol.flags.get("rotf")
|
||||||
|
if v:
|
||||||
|
use = True
|
||||||
|
lim.set_rotf(v)
|
||||||
|
|
||||||
|
v = vol.flags.get("maxn")
|
||||||
|
if v:
|
||||||
|
use = True
|
||||||
|
lim.nmax, lim.nwin = [int(x) for x in v.split(",")]
|
||||||
|
|
||||||
|
v = vol.flags.get("maxb")
|
||||||
|
if v:
|
||||||
|
use = True
|
||||||
|
lim.bmax, lim.bwin = [unhumanize(x) for x in v.split(",")]
|
||||||
|
|
||||||
|
if use:
|
||||||
|
vol.lim = lim
|
||||||
|
|
||||||
|
for vol in vfs.all_vols.values():
|
||||||
|
if "pk" in vol.flags and "gz" not in vol.flags and "xz" not in vol.flags:
|
||||||
|
vol.flags["gz"] = False # def.pk
|
||||||
|
|
||||||
|
if "scan" in vol.flags:
|
||||||
|
vol.flags["scan"] = int(vol.flags["scan"])
|
||||||
|
elif self.args.re_maxage:
|
||||||
|
vol.flags["scan"] = self.args.re_maxage
|
||||||
|
|
||||||
all_mte = {}
|
all_mte = {}
|
||||||
errors = False
|
errors = False
|
||||||
for vol in vfs.all_vols.values():
|
for vol in vfs.all_vols.values():
|
||||||
if (self.args.e2ds and vol.uwrite) or self.args.e2dsa:
|
if (self.args.e2ds and vol.axs.uwrite) or self.args.e2dsa:
|
||||||
vol.flags["e2ds"] = True
|
vol.flags["e2ds"] = True
|
||||||
|
|
||||||
if self.args.e2d or "e2ds" in vol.flags:
|
if self.args.e2d or "e2ds" in vol.flags:
|
||||||
@@ -629,9 +846,11 @@ class AuthSrv(object):
|
|||||||
if k1 in vol.flags:
|
if k1 in vol.flags:
|
||||||
vol.flags[k2] = True
|
vol.flags[k2] = True
|
||||||
|
|
||||||
# default tag-list if unset
|
# default tag cfgs if unset
|
||||||
if "mte" not in vol.flags:
|
if "mte" not in vol.flags:
|
||||||
vol.flags["mte"] = self.args.mte
|
vol.flags["mte"] = self.args.mte
|
||||||
|
if "mth" not in vol.flags:
|
||||||
|
vol.flags["mth"] = self.args.mth
|
||||||
|
|
||||||
# append parsers from argv to volume-flags
|
# append parsers from argv to volume-flags
|
||||||
self._read_volflag(vol.flags, "mtp", self.args.mtp, True)
|
self._read_volflag(vol.flags, "mtp", self.args.mtp, True)
|
||||||
@@ -700,6 +919,27 @@ class AuthSrv(object):
|
|||||||
|
|
||||||
vfs.bubble_flags()
|
vfs.bubble_flags()
|
||||||
|
|
||||||
|
m = "volumes and permissions:\n"
|
||||||
|
for v in vfs.all_vols.values():
|
||||||
|
if not self.warn_anonwrite:
|
||||||
|
break
|
||||||
|
|
||||||
|
m += '\n\033[36m"/{}" \033[33m{}\033[0m'.format(v.vpath, v.realpath)
|
||||||
|
for txt, attr in [
|
||||||
|
[" read", "uread"],
|
||||||
|
[" write", "uwrite"],
|
||||||
|
[" move", "umove"],
|
||||||
|
["delete", "udel"],
|
||||||
|
]:
|
||||||
|
u = list(sorted(getattr(v.axs, attr).keys()))
|
||||||
|
u = ", ".join("\033[35meverybody\033[0m" if x == "*" else x for x in u)
|
||||||
|
u = u if u else "\033[36m--none--\033[0m"
|
||||||
|
m += "\n| {}: {}".format(txt, u)
|
||||||
|
m += "\n"
|
||||||
|
|
||||||
|
if self.warn_anonwrite and not self.args.no_voldump:
|
||||||
|
self.log(m)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
v, _ = vfs.get("/", "*", False, True)
|
v, _ = vfs.get("/", "*", False, True)
|
||||||
if self.warn_anonwrite and os.getcwd() == v.realpath:
|
if self.warn_anonwrite and os.getcwd() == v.realpath:
|
||||||
@@ -711,17 +951,14 @@ class AuthSrv(object):
|
|||||||
|
|
||||||
with self.mutex:
|
with self.mutex:
|
||||||
self.vfs = vfs
|
self.vfs = vfs
|
||||||
self.user = user
|
self.acct = acct
|
||||||
self.iuser = {v: k for k, v in user.items()}
|
self.iacct = {v: k for k, v in acct.items()}
|
||||||
|
|
||||||
self.re_pwd = None
|
self.re_pwd = None
|
||||||
pwds = [re.escape(x) for x in self.iuser.keys()]
|
pwds = [re.escape(x) for x in self.iacct.keys()]
|
||||||
if pwds:
|
if pwds:
|
||||||
self.re_pwd = re.compile("=(" + "|".join(pwds) + ")([]&; ]|$)")
|
self.re_pwd = re.compile("=(" + "|".join(pwds) + ")([]&; ]|$)")
|
||||||
|
|
||||||
# import pprint
|
|
||||||
# pprint.pprint({"usr": user, "rd": mread, "wr": mwrite, "mnt": mount})
|
|
||||||
|
|
||||||
def dbg_ls(self):
|
def dbg_ls(self):
|
||||||
users = self.args.ls
|
users = self.args.ls
|
||||||
vols = "*"
|
vols = "*"
|
||||||
@@ -739,12 +976,12 @@ class AuthSrv(object):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
if users == "**":
|
if users == "**":
|
||||||
users = list(self.user.keys()) + ["*"]
|
users = list(self.acct.keys()) + ["*"]
|
||||||
else:
|
else:
|
||||||
users = [users]
|
users = [users]
|
||||||
|
|
||||||
for u in users:
|
for u in users:
|
||||||
if u not in self.user and u != "*":
|
if u not in self.acct and u != "*":
|
||||||
raise Exception("user not found: " + u)
|
raise Exception("user not found: " + u)
|
||||||
|
|
||||||
if vols == "*":
|
if vols == "*":
|
||||||
@@ -760,8 +997,10 @@ class AuthSrv(object):
|
|||||||
raise Exception("volume not found: " + v)
|
raise Exception("volume not found: " + v)
|
||||||
|
|
||||||
self.log({"users": users, "vols": vols, "flags": flags})
|
self.log({"users": users, "vols": vols, "flags": flags})
|
||||||
|
m = "/{}: read({}) write({}) move({}) del({})"
|
||||||
for k, v in self.vfs.all_vols.items():
|
for k, v in self.vfs.all_vols.items():
|
||||||
self.log("/{}: read({}) write({})".format(k, v.uread, v.uwrite))
|
vc = v.axs
|
||||||
|
self.log(m.format(k, vc.uread, vc.uwrite, vc.umove, vc.udel))
|
||||||
|
|
||||||
flag_v = "v" in flags
|
flag_v = "v" in flags
|
||||||
flag_ln = "ln" in flags
|
flag_ln = "ln" in flags
|
||||||
@@ -775,13 +1014,15 @@ class AuthSrv(object):
|
|||||||
for u in users:
|
for u in users:
|
||||||
self.log("checking /{} as {}".format(v, u))
|
self.log("checking /{} as {}".format(v, u))
|
||||||
try:
|
try:
|
||||||
vn, _ = self.vfs.get(v, u, True, False)
|
vn, _ = self.vfs.get(v, u, True, False, False, False)
|
||||||
except:
|
except:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
atop = vn.realpath
|
atop = vn.realpath
|
||||||
g = vn.walk("", "", [], u, True, not self.args.no_scandir, False)
|
g = vn.walk(
|
||||||
for vpath, apath, files, _, _ in g:
|
vn.vpath, "", [], u, [[True]], True, not self.args.no_scandir, False
|
||||||
|
)
|
||||||
|
for _, _, vpath, apath, files, _, _ in g:
|
||||||
fnames = [n[0] for n in files]
|
fnames = [n[0] for n in files]
|
||||||
vpaths = [vpath + "/" + n for n in fnames] if vpath else fnames
|
vpaths = [vpath + "/" + n for n in fnames] if vpath else fnames
|
||||||
vpaths = [vtop + x for x in vpaths]
|
vpaths = [vtop + x for x in vpaths]
|
||||||
|
|||||||
0
copyparty/bos/__init__.py
Normal file
0
copyparty/bos/__init__.py
Normal file
59
copyparty/bos/bos.py
Normal file
59
copyparty/bos/bos.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import print_function, unicode_literals
|
||||||
|
|
||||||
|
import os
|
||||||
|
from ..util import fsenc, fsdec
|
||||||
|
from . import path
|
||||||
|
|
||||||
|
|
||||||
|
# grep -hRiE '(^|[^a-zA-Z_\.-])os\.' . | gsed -r 's/ /\n/g;s/\(/(\n/g' | grep -hRiE '(^|[^a-zA-Z_\.-])os\.' | sort | uniq -c
|
||||||
|
# printf 'os\.(%s)' "$(grep ^def bos/__init__.py | gsed -r 's/^def //;s/\(.*//' | tr '\n' '|' | gsed -r 's/.$//')"
|
||||||
|
|
||||||
|
|
||||||
|
def chmod(p, mode):
|
||||||
|
return os.chmod(fsenc(p), mode)
|
||||||
|
|
||||||
|
|
||||||
|
def listdir(p="."):
|
||||||
|
return [fsdec(x) for x in os.listdir(fsenc(p))]
|
||||||
|
|
||||||
|
|
||||||
|
def lstat(p):
|
||||||
|
return os.lstat(fsenc(p))
|
||||||
|
|
||||||
|
|
||||||
|
def makedirs(name, mode=0o755, exist_ok=True):
|
||||||
|
bname = fsenc(name)
|
||||||
|
try:
|
||||||
|
os.makedirs(bname, mode=mode)
|
||||||
|
except:
|
||||||
|
if not exist_ok or not os.path.isdir(bname):
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def mkdir(p, mode=0o755):
|
||||||
|
return os.mkdir(fsenc(p), mode=mode)
|
||||||
|
|
||||||
|
|
||||||
|
def rename(src, dst):
|
||||||
|
return os.rename(fsenc(src), fsenc(dst))
|
||||||
|
|
||||||
|
|
||||||
|
def replace(src, dst):
|
||||||
|
return os.replace(fsenc(src), fsenc(dst))
|
||||||
|
|
||||||
|
|
||||||
|
def rmdir(p):
|
||||||
|
return os.rmdir(fsenc(p))
|
||||||
|
|
||||||
|
|
||||||
|
def stat(p):
|
||||||
|
return os.stat(fsenc(p))
|
||||||
|
|
||||||
|
|
||||||
|
def unlink(p):
|
||||||
|
return os.unlink(fsenc(p))
|
||||||
|
|
||||||
|
|
||||||
|
def utime(p, times=None):
|
||||||
|
return os.utime(fsenc(p), times)
|
||||||
33
copyparty/bos/path.py
Normal file
33
copyparty/bos/path.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
from __future__ import print_function, unicode_literals
|
||||||
|
|
||||||
|
import os
|
||||||
|
from ..util import fsenc, fsdec
|
||||||
|
|
||||||
|
|
||||||
|
def abspath(p):
|
||||||
|
return fsdec(os.path.abspath(fsenc(p)))
|
||||||
|
|
||||||
|
|
||||||
|
def exists(p):
|
||||||
|
return os.path.exists(fsenc(p))
|
||||||
|
|
||||||
|
|
||||||
|
def getmtime(p):
|
||||||
|
return os.path.getmtime(fsenc(p))
|
||||||
|
|
||||||
|
|
||||||
|
def getsize(p):
|
||||||
|
return os.path.getsize(fsenc(p))
|
||||||
|
|
||||||
|
|
||||||
|
def isdir(p):
|
||||||
|
return os.path.isdir(fsenc(p))
|
||||||
|
|
||||||
|
|
||||||
|
def islink(p):
|
||||||
|
return os.path.islink(fsenc(p))
|
||||||
|
|
||||||
|
|
||||||
|
def realpath(p):
|
||||||
|
return fsdec(os.path.realpath(fsenc(p)))
|
||||||
@@ -22,23 +22,19 @@ class BrokerMp(object):
|
|||||||
self.retpend_mutex = threading.Lock()
|
self.retpend_mutex = threading.Lock()
|
||||||
self.mutex = threading.Lock()
|
self.mutex = threading.Lock()
|
||||||
|
|
||||||
cores = self.args.j
|
self.num_workers = self.args.j or mp.cpu_count()
|
||||||
if not cores:
|
self.log("broker", "booting {} subprocesses".format(self.num_workers))
|
||||||
cores = mp.cpu_count()
|
for n in range(1, self.num_workers + 1):
|
||||||
|
|
||||||
self.log("broker", "booting {} subprocesses".format(cores))
|
|
||||||
for n in range(cores):
|
|
||||||
q_pend = mp.Queue(1)
|
q_pend = mp.Queue(1)
|
||||||
q_yield = mp.Queue(64)
|
q_yield = mp.Queue(64)
|
||||||
|
|
||||||
proc = mp.Process(target=MpWorker, args=(q_pend, q_yield, self.args, n))
|
proc = mp.Process(target=MpWorker, args=(q_pend, q_yield, self.args, n))
|
||||||
proc.q_pend = q_pend
|
proc.q_pend = q_pend
|
||||||
proc.q_yield = q_yield
|
proc.q_yield = q_yield
|
||||||
proc.nid = n
|
|
||||||
proc.clients = {}
|
proc.clients = {}
|
||||||
|
|
||||||
thr = threading.Thread(
|
thr = threading.Thread(
|
||||||
target=self.collector, args=(proc,), name="mp-collector"
|
target=self.collector, args=(proc,), name="mp-sink-{}".format(n)
|
||||||
)
|
)
|
||||||
thr.daemon = True
|
thr.daemon = True
|
||||||
thr.start()
|
thr.start()
|
||||||
@@ -104,5 +100,8 @@ class BrokerMp(object):
|
|||||||
for p in self.procs:
|
for p in self.procs:
|
||||||
p.q_pend.put([0, dest, [args[0], len(self.procs)]])
|
p.q_pend.put([0, dest, [args[0], len(self.procs)]])
|
||||||
|
|
||||||
|
elif dest == "cb_httpsrv_up":
|
||||||
|
self.hub.cb_httpsrv_up()
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise Exception("what is " + str(dest))
|
raise Exception("what is " + str(dest))
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
from __future__ import print_function, unicode_literals
|
from __future__ import print_function, unicode_literals
|
||||||
from copyparty.authsrv import AuthSrv
|
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import signal
|
import signal
|
||||||
@@ -9,6 +8,7 @@ import threading
|
|||||||
from .broker_util import ExceptionalQueue
|
from .broker_util import ExceptionalQueue
|
||||||
from .httpsrv import HttpSrv
|
from .httpsrv import HttpSrv
|
||||||
from .util import FAKE_MP
|
from .util import FAKE_MP
|
||||||
|
from copyparty.authsrv import AuthSrv
|
||||||
|
|
||||||
|
|
||||||
class MpWorker(object):
|
class MpWorker(object):
|
||||||
@@ -29,13 +29,14 @@ class MpWorker(object):
|
|||||||
# we inherited signal_handler from parent,
|
# we inherited signal_handler from parent,
|
||||||
# replace it with something harmless
|
# replace it with something harmless
|
||||||
if not FAKE_MP:
|
if not FAKE_MP:
|
||||||
signal.signal(signal.SIGINT, self.signal_handler)
|
for sig in [signal.SIGINT, signal.SIGTERM]:
|
||||||
|
signal.signal(sig, self.signal_handler)
|
||||||
|
|
||||||
# starting to look like a good idea
|
# starting to look like a good idea
|
||||||
self.asrv = AuthSrv(args, None, False)
|
self.asrv = AuthSrv(args, None, False)
|
||||||
|
|
||||||
# instantiate all services here (TODO: inheritance?)
|
# instantiate all services here (TODO: inheritance?)
|
||||||
self.httpsrv = HttpSrv(self, True)
|
self.httpsrv = HttpSrv(self, n)
|
||||||
|
|
||||||
# on winxp and some other platforms,
|
# on winxp and some other platforms,
|
||||||
# use thr.join() to block all signals
|
# use thr.join() to block all signals
|
||||||
@@ -44,7 +45,7 @@ class MpWorker(object):
|
|||||||
thr.start()
|
thr.start()
|
||||||
thr.join()
|
thr.join()
|
||||||
|
|
||||||
def signal_handler(self, signal, frame):
|
def signal_handler(self, sig, frame):
|
||||||
# print('k')
|
# print('k')
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -17,9 +17,10 @@ class BrokerThr(object):
|
|||||||
self.asrv = hub.asrv
|
self.asrv = hub.asrv
|
||||||
|
|
||||||
self.mutex = threading.Lock()
|
self.mutex = threading.Lock()
|
||||||
|
self.num_workers = 1
|
||||||
|
|
||||||
# instantiate all services here (TODO: inheritance?)
|
# instantiate all services here (TODO: inheritance?)
|
||||||
self.httpsrv = HttpSrv(self)
|
self.httpsrv = HttpSrv(self, None)
|
||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
# self.log("broker", "shutting down")
|
# self.log("broker", "shutting down")
|
||||||
|
|||||||
@@ -7,15 +7,22 @@ import gzip
|
|||||||
import time
|
import time
|
||||||
import copy
|
import copy
|
||||||
import json
|
import json
|
||||||
|
import base64
|
||||||
import string
|
import string
|
||||||
import socket
|
import socket
|
||||||
import ctypes
|
import ctypes
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import calendar
|
import calendar
|
||||||
|
|
||||||
|
try:
|
||||||
|
import lzma
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
from .__init__ import E, PY2, WINDOWS, ANYWIN, unicode
|
from .__init__ import E, PY2, WINDOWS, ANYWIN, unicode
|
||||||
from .util import * # noqa # pylint: disable=unused-wildcard-import
|
from .util import * # noqa # pylint: disable=unused-wildcard-import
|
||||||
from .authsrv import AuthSrv
|
from .bos import bos
|
||||||
|
from .authsrv import AuthSrv, Lim
|
||||||
from .szip import StreamZip
|
from .szip import StreamZip
|
||||||
from .star import StreamTar
|
from .star import StreamTar
|
||||||
|
|
||||||
@@ -37,7 +44,6 @@ class HttpCli(object):
|
|||||||
self.ip = conn.addr[0]
|
self.ip = conn.addr[0]
|
||||||
self.addr = conn.addr # type: tuple[str, int]
|
self.addr = conn.addr # type: tuple[str, int]
|
||||||
self.args = conn.args
|
self.args = conn.args
|
||||||
self.is_mp = conn.is_mp
|
|
||||||
self.asrv = conn.asrv # type: AuthSrv
|
self.asrv = conn.asrv # type: AuthSrv
|
||||||
self.ico = conn.ico
|
self.ico = conn.ico
|
||||||
self.thumbcli = conn.thumbcli
|
self.thumbcli = conn.thumbcli
|
||||||
@@ -59,9 +65,12 @@ class HttpCli(object):
|
|||||||
|
|
||||||
def unpwd(self, m):
|
def unpwd(self, m):
|
||||||
a, b = m.groups()
|
a, b = m.groups()
|
||||||
return "=\033[7m {} \033[27m{}".format(self.asrv.iuser[a], b)
|
return "=\033[7m {} \033[27m{}".format(self.asrv.iacct[a], b)
|
||||||
|
|
||||||
|
def _check_nonfatal(self, ex, post):
|
||||||
|
if post:
|
||||||
|
return ex.code < 300
|
||||||
|
|
||||||
def _check_nonfatal(self, ex):
|
|
||||||
return ex.code < 400 or ex.code in [404, 429]
|
return ex.code < 400 or ex.code in [404, 429]
|
||||||
|
|
||||||
def _assert_safe_rem(self, rem):
|
def _assert_safe_rem(self, rem):
|
||||||
@@ -103,8 +112,8 @@ class HttpCli(object):
|
|||||||
self.req = "[junk]"
|
self.req = "[junk]"
|
||||||
self.http_ver = "HTTP/1.1"
|
self.http_ver = "HTTP/1.1"
|
||||||
# self.log("pebkac at httpcli.run #1: " + repr(ex))
|
# self.log("pebkac at httpcli.run #1: " + repr(ex))
|
||||||
self.keepalive = self._check_nonfatal(ex)
|
self.keepalive = False
|
||||||
self.loud_reply(unicode(ex), status=ex.code)
|
self.loud_reply(unicode(ex), status=ex.code, volsan=True)
|
||||||
return self.keepalive
|
return self.keepalive
|
||||||
|
|
||||||
# time.sleep(0.4)
|
# time.sleep(0.4)
|
||||||
@@ -177,14 +186,33 @@ class HttpCli(object):
|
|||||||
if kc in cookies and ku not in uparam:
|
if kc in cookies and ku not in uparam:
|
||||||
uparam[ku] = cookies[kc]
|
uparam[ku] = cookies[kc]
|
||||||
|
|
||||||
|
if len(uparam) > 10 or len(cookies) > 50:
|
||||||
|
raise Pebkac(400, "u wot m8")
|
||||||
|
|
||||||
self.uparam = uparam
|
self.uparam = uparam
|
||||||
self.cookies = cookies
|
self.cookies = cookies
|
||||||
self.vpath = unquotep(vpath)
|
self.vpath = unquotep(vpath) # not query, so + means +
|
||||||
|
|
||||||
pwd = uparam.get("pw")
|
pwd = None
|
||||||
self.uname = self.asrv.iuser.get(pwd, "*")
|
ba = self.headers.get("authorization")
|
||||||
self.rvol, self.wvol, self.avol = [[], [], []]
|
if ba:
|
||||||
self.asrv.vfs.user_tree(self.uname, self.rvol, self.wvol, self.avol)
|
try:
|
||||||
|
ba = ba.split(" ")[1].encode("ascii")
|
||||||
|
ba = base64.b64decode(ba).decode("utf-8")
|
||||||
|
# try "pwd", "x:pwd", "pwd:x"
|
||||||
|
for ba in [ba] + ba.split(":", 1)[::-1]:
|
||||||
|
if self.asrv.iacct.get(ba):
|
||||||
|
pwd = ba
|
||||||
|
break
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
pwd = uparam.get("pw") or pwd
|
||||||
|
self.uname = self.asrv.iacct.get(pwd, "*")
|
||||||
|
self.rvol = self.asrv.vfs.aread[self.uname]
|
||||||
|
self.wvol = self.asrv.vfs.awrite[self.uname]
|
||||||
|
self.mvol = self.asrv.vfs.amove[self.uname]
|
||||||
|
self.dvol = self.asrv.vfs.adel[self.uname]
|
||||||
|
|
||||||
if pwd and "pw" in self.ouparam and pwd != cookies.get("cppwd"):
|
if pwd and "pw" in self.ouparam and pwd != cookies.get("cppwd"):
|
||||||
self.out_headers["Set-Cookie"] = self.get_pwd_cookie(pwd)[0]
|
self.out_headers["Set-Cookie"] = self.get_pwd_cookie(pwd)[0]
|
||||||
@@ -211,23 +239,29 @@ class HttpCli(object):
|
|||||||
else:
|
else:
|
||||||
raise Pebkac(400, 'invalid HTTP mode "{0}"'.format(self.mode))
|
raise Pebkac(400, 'invalid HTTP mode "{0}"'.format(self.mode))
|
||||||
|
|
||||||
except Pebkac as ex:
|
except Exception as ex:
|
||||||
|
pex = ex
|
||||||
|
if not hasattr(ex, "code"):
|
||||||
|
pex = Pebkac(500)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# self.log("pebkac at httpcli.run #2: " + repr(ex))
|
post = self.mode in ["POST", "PUT"] or "content-length" in self.headers
|
||||||
if not self._check_nonfatal(ex):
|
if not self._check_nonfatal(pex, post):
|
||||||
self.keepalive = False
|
self.keepalive = False
|
||||||
|
|
||||||
self.log("{}\033[0m, {}".format(str(ex), self.vpath), 3)
|
msg = str(ex) if pex == ex else min_ex()
|
||||||
|
self.log("{}\033[0m, {}".format(msg, self.vpath), 3)
|
||||||
|
|
||||||
msg = "<pre>{}\r\nURL: {}\r\n".format(str(ex), self.vpath)
|
msg = "<pre>{}\r\nURL: {}\r\n".format(str(ex), self.vpath)
|
||||||
if self.hint:
|
if self.hint:
|
||||||
msg += "hint: {}\r\n".format(self.hint)
|
msg += "hint: {}\r\n".format(self.hint)
|
||||||
|
|
||||||
self.reply(msg.encode("utf-8", "replace"), status=ex.code)
|
self.reply(msg.encode("utf-8", "replace"), status=pex.code, volsan=True)
|
||||||
return self.keepalive
|
return self.keepalive
|
||||||
except Pebkac:
|
except Pebkac:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def send_headers(self, length, status=200, mime=None, headers={}):
|
def send_headers(self, length, status=200, mime=None, headers=None):
|
||||||
response = ["{} {} {}".format(self.http_ver, status, HTTPCODE[status])]
|
response = ["{} {} {}".format(self.http_ver, status, HTTPCODE[status])]
|
||||||
|
|
||||||
if length is not None:
|
if length is not None:
|
||||||
@@ -237,6 +271,7 @@ class HttpCli(object):
|
|||||||
response.append("Connection: " + ("Keep-Alive" if self.keepalive else "Close"))
|
response.append("Connection: " + ("Keep-Alive" if self.keepalive else "Close"))
|
||||||
|
|
||||||
# headers{} overrides anything set previously
|
# headers{} overrides anything set previously
|
||||||
|
if headers:
|
||||||
self.out_headers.update(headers)
|
self.out_headers.update(headers)
|
||||||
|
|
||||||
# default to utf8 html if no content-type is set
|
# default to utf8 html if no content-type is set
|
||||||
@@ -254,8 +289,12 @@ class HttpCli(object):
|
|||||||
except:
|
except:
|
||||||
raise Pebkac(400, "client d/c while replying headers")
|
raise Pebkac(400, "client d/c while replying headers")
|
||||||
|
|
||||||
def reply(self, body, status=200, mime=None, headers={}):
|
def reply(self, body, status=200, mime=None, headers=None, volsan=False):
|
||||||
# TODO something to reply with user-supplied values safely
|
# TODO something to reply with user-supplied values safely
|
||||||
|
|
||||||
|
if volsan:
|
||||||
|
body = vol_san(self.asrv.vfs.all_vols.values(), body)
|
||||||
|
|
||||||
self.send_headers(len(body), status, mime, headers)
|
self.send_headers(len(body), status, mime, headers)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -270,7 +309,7 @@ class HttpCli(object):
|
|||||||
self.log(body.rstrip())
|
self.log(body.rstrip())
|
||||||
self.reply(b"<pre>" + body.encode("utf-8") + b"\r\n", *list(args), **kwargs)
|
self.reply(b"<pre>" + body.encode("utf-8") + b"\r\n", *list(args), **kwargs)
|
||||||
|
|
||||||
def urlq(self, add={}, rm=[]):
|
def urlq(self, add, rm):
|
||||||
"""
|
"""
|
||||||
generates url query based on uparam (b, pw, all others)
|
generates url query based on uparam (b, pw, all others)
|
||||||
removing anything in rm, adding pairs in add
|
removing anything in rm, adding pairs in add
|
||||||
@@ -339,9 +378,37 @@ class HttpCli(object):
|
|||||||
static_path = os.path.join(E.mod, "web/", self.vpath[5:])
|
static_path = os.path.join(E.mod, "web/", self.vpath[5:])
|
||||||
return self.tx_file(static_path)
|
return self.tx_file(static_path)
|
||||||
|
|
||||||
|
x = self.asrv.vfs.can_access(self.vpath, self.uname)
|
||||||
|
self.can_read, self.can_write, self.can_move, self.can_delete = x
|
||||||
|
if not self.can_read and not self.can_write:
|
||||||
|
if self.vpath:
|
||||||
|
self.log("inaccessible: [{}]".format(self.vpath))
|
||||||
|
raise Pebkac(404)
|
||||||
|
|
||||||
|
self.uparam["h"] = False
|
||||||
|
|
||||||
if "tree" in self.uparam:
|
if "tree" in self.uparam:
|
||||||
return self.tx_tree()
|
return self.tx_tree()
|
||||||
|
|
||||||
|
if "delete" in self.uparam:
|
||||||
|
return self.handle_rm()
|
||||||
|
|
||||||
|
if "move" in self.uparam:
|
||||||
|
return self.handle_mv()
|
||||||
|
|
||||||
|
if "scan" in self.uparam:
|
||||||
|
return self.scanvol()
|
||||||
|
|
||||||
|
if not self.vpath:
|
||||||
|
if "stack" in self.uparam:
|
||||||
|
return self.tx_stack()
|
||||||
|
|
||||||
|
if "ups" in self.uparam:
|
||||||
|
return self.tx_ups()
|
||||||
|
|
||||||
|
if "h" in self.uparam:
|
||||||
|
return self.tx_mounts()
|
||||||
|
|
||||||
# conditional redirect to single volumes
|
# conditional redirect to single volumes
|
||||||
if self.vpath == "" and not self.ouparam:
|
if self.vpath == "" and not self.ouparam:
|
||||||
nread = len(self.rvol)
|
nread = len(self.rvol)
|
||||||
@@ -356,24 +423,6 @@ class HttpCli(object):
|
|||||||
self.redirect(vpath, flavor="redirecting to", use302=True)
|
self.redirect(vpath, flavor="redirecting to", use302=True)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
self.readable, self.writable = self.asrv.vfs.can_access(self.vpath, self.uname)
|
|
||||||
if not self.readable and not self.writable:
|
|
||||||
if self.vpath:
|
|
||||||
self.log("inaccessible: [{}]".format(self.vpath))
|
|
||||||
raise Pebkac(404)
|
|
||||||
|
|
||||||
self.uparam = {"h": False}
|
|
||||||
|
|
||||||
if "h" in self.uparam:
|
|
||||||
self.vpath = None
|
|
||||||
return self.tx_mounts()
|
|
||||||
|
|
||||||
if "scan" in self.uparam:
|
|
||||||
return self.scanvol()
|
|
||||||
|
|
||||||
if "stack" in self.uparam:
|
|
||||||
return self.tx_stack()
|
|
||||||
|
|
||||||
return self.tx_browser()
|
return self.tx_browser()
|
||||||
|
|
||||||
def handle_options(self):
|
def handle_options(self):
|
||||||
@@ -456,7 +505,7 @@ class HttpCli(object):
|
|||||||
if "get" in opt:
|
if "get" in opt:
|
||||||
return self.handle_get()
|
return self.handle_get()
|
||||||
|
|
||||||
raise Pebkac(405, "POST({}) is disabled".format(ctype))
|
raise Pebkac(405, "POST({}) is disabled in server config".format(ctype))
|
||||||
|
|
||||||
raise Pebkac(405, "don't know how to handle POST({})".format(ctype))
|
raise Pebkac(405, "don't know how to handle POST({})".format(ctype))
|
||||||
|
|
||||||
@@ -474,7 +523,11 @@ class HttpCli(object):
|
|||||||
def dump_to_file(self):
|
def dump_to_file(self):
|
||||||
reader, remains = self.get_body_reader()
|
reader, remains = self.get_body_reader()
|
||||||
vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True)
|
vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True)
|
||||||
|
lim = vfs.get_dbv(rem)[0].lim
|
||||||
fdir = os.path.join(vfs.realpath, rem)
|
fdir = os.path.join(vfs.realpath, rem)
|
||||||
|
if lim:
|
||||||
|
fdir, rem = lim.all(self.ip, rem, remains, fdir)
|
||||||
|
bos.makedirs(fdir)
|
||||||
|
|
||||||
addr = self.ip.replace(":", ".")
|
addr = self.ip.replace(":", ".")
|
||||||
fn = "put-{:.6f}-{}.bin".format(time.time(), addr)
|
fn = "put-{:.6f}-{}.bin".format(time.time(), addr)
|
||||||
@@ -482,13 +535,81 @@ class HttpCli(object):
|
|||||||
if self.args.nw:
|
if self.args.nw:
|
||||||
path = os.devnull
|
path = os.devnull
|
||||||
|
|
||||||
with open(fsenc(path), "wb", 512 * 1024) as f:
|
open_f = open
|
||||||
|
open_a = [fsenc(path), "wb", 512 * 1024]
|
||||||
|
open_ka = {}
|
||||||
|
|
||||||
|
# user-request || config-force
|
||||||
|
if ("gz" in vfs.flags or "xz" in vfs.flags) and (
|
||||||
|
"pk" in vfs.flags
|
||||||
|
or "pk" in self.uparam
|
||||||
|
or "gz" in self.uparam
|
||||||
|
or "xz" in self.uparam
|
||||||
|
):
|
||||||
|
fb = {"gz": 9, "xz": 0} # default/fallback level
|
||||||
|
lv = {} # selected level
|
||||||
|
alg = None # selected algo (gz=preferred)
|
||||||
|
|
||||||
|
# user-prefs first
|
||||||
|
if "gz" in self.uparam or "pk" in self.uparam: # def.pk
|
||||||
|
alg = "gz"
|
||||||
|
if "xz" in self.uparam:
|
||||||
|
alg = "xz"
|
||||||
|
if alg:
|
||||||
|
v = self.uparam.get(alg)
|
||||||
|
lv[alg] = fb[alg] if v is None else int(v)
|
||||||
|
|
||||||
|
if alg not in vfs.flags:
|
||||||
|
alg = "gz" if "gz" in vfs.flags else "xz"
|
||||||
|
|
||||||
|
# then server overrides
|
||||||
|
pk = vfs.flags.get("pk")
|
||||||
|
if pk is not None:
|
||||||
|
# config-forced on
|
||||||
|
alg = alg or "gz" # def.pk
|
||||||
|
try:
|
||||||
|
# config-forced opts
|
||||||
|
alg, lv = pk.split(",")
|
||||||
|
lv[alg] = int(lv)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
lv[alg] = lv.get(alg) or fb.get(alg)
|
||||||
|
|
||||||
|
self.log("compressing with {} level {}".format(alg, lv.get(alg)))
|
||||||
|
if alg == "gz":
|
||||||
|
open_f = gzip.GzipFile
|
||||||
|
open_a = [fsenc(path), "wb", lv[alg], None, 0x5FEE6600] # 2021-01-01
|
||||||
|
elif alg == "xz":
|
||||||
|
open_f = lzma.open
|
||||||
|
open_a = [fsenc(path), "wb"]
|
||||||
|
open_ka = {"preset": lv[alg]}
|
||||||
|
else:
|
||||||
|
self.log("fallthrough? thats a bug", 1)
|
||||||
|
|
||||||
|
with open_f(*open_a, **open_ka) as f:
|
||||||
post_sz, _, sha_b64 = hashcopy(reader, f)
|
post_sz, _, sha_b64 = hashcopy(reader, f)
|
||||||
|
|
||||||
|
if lim:
|
||||||
|
lim.nup(self.ip)
|
||||||
|
lim.bup(self.ip, post_sz)
|
||||||
|
try:
|
||||||
|
lim.chk_sz(post_sz)
|
||||||
|
except:
|
||||||
|
bos.unlink(path)
|
||||||
|
raise
|
||||||
|
|
||||||
if not self.args.nw:
|
if not self.args.nw:
|
||||||
vfs, vrem = vfs.get_dbv(rem)
|
vfs, vrem = vfs.get_dbv(rem)
|
||||||
self.conn.hsrv.broker.put(
|
self.conn.hsrv.broker.put(
|
||||||
False, "up2k.hash_file", vfs.realpath, vfs.flags, vrem, fn
|
False,
|
||||||
|
"up2k.hash_file",
|
||||||
|
vfs.realpath,
|
||||||
|
vfs.flags,
|
||||||
|
vrem,
|
||||||
|
fn,
|
||||||
|
self.ip,
|
||||||
|
time.time(),
|
||||||
)
|
)
|
||||||
|
|
||||||
return post_sz, sha_b64, remains, path
|
return post_sz, sha_b64, remains, path
|
||||||
@@ -559,7 +680,7 @@ class HttpCli(object):
|
|||||||
try:
|
try:
|
||||||
remains = int(self.headers["content-length"])
|
remains = int(self.headers["content-length"])
|
||||||
except:
|
except:
|
||||||
raise Pebkac(400, "you must supply a content-length for JSON POST")
|
raise Pebkac(411)
|
||||||
|
|
||||||
if remains > 1024 * 1024:
|
if remains > 1024 * 1024:
|
||||||
raise Pebkac(413, "json 2big")
|
raise Pebkac(413, "json 2big")
|
||||||
@@ -582,17 +703,17 @@ class HttpCli(object):
|
|||||||
if "srch" in self.uparam or "srch" in body:
|
if "srch" in self.uparam or "srch" in body:
|
||||||
return self.handle_search(body)
|
return self.handle_search(body)
|
||||||
|
|
||||||
|
if "delete" in self.uparam:
|
||||||
|
return self.handle_rm(body)
|
||||||
|
|
||||||
# up2k-php compat
|
# up2k-php compat
|
||||||
for k in "chunkpit.php", "handshake.php":
|
for k in "chunkpit.php", "handshake.php":
|
||||||
if self.vpath.endswith(k):
|
if self.vpath.endswith(k):
|
||||||
self.vpath = self.vpath[: -len(k)]
|
self.vpath = self.vpath[: -len(k)]
|
||||||
|
|
||||||
sub = None
|
|
||||||
name = undot(body["name"])
|
name = undot(body["name"])
|
||||||
if "/" in name:
|
if "/" in name:
|
||||||
sub, name = name.rsplit("/", 1)
|
raise Pebkac(400, "your client is old; press CTRL-SHIFT-R and try again")
|
||||||
self.vpath = "/".join([self.vpath, sub]).strip("/")
|
|
||||||
body["name"] = name
|
|
||||||
|
|
||||||
vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True)
|
vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True)
|
||||||
dbv, vrem = vfs.get_dbv(rem)
|
dbv, vrem = vfs.get_dbv(rem)
|
||||||
@@ -603,14 +724,14 @@ class HttpCli(object):
|
|||||||
body["addr"] = self.ip
|
body["addr"] = self.ip
|
||||||
body["vcfg"] = dbv.flags
|
body["vcfg"] = dbv.flags
|
||||||
|
|
||||||
if sub:
|
if rem:
|
||||||
try:
|
try:
|
||||||
dst = os.path.join(vfs.realpath, rem)
|
dst = os.path.join(vfs.realpath, rem)
|
||||||
if not os.path.isdir(fsenc(dst)):
|
if not bos.path.isdir(dst):
|
||||||
os.makedirs(fsenc(dst))
|
bos.makedirs(dst)
|
||||||
except OSError as ex:
|
except OSError as ex:
|
||||||
self.log("makedirs failed [{}]".format(dst))
|
self.log("makedirs failed [{}]".format(dst))
|
||||||
if not os.path.isdir(fsenc(dst)):
|
if not bos.path.isdir(dst):
|
||||||
if ex.errno == 13:
|
if ex.errno == 13:
|
||||||
raise Pebkac(500, "the server OS denied write-access")
|
raise Pebkac(500, "the server OS denied write-access")
|
||||||
|
|
||||||
@@ -623,9 +744,6 @@ class HttpCli(object):
|
|||||||
|
|
||||||
x = self.conn.hsrv.broker.put(True, "up2k.handle_json", body)
|
x = self.conn.hsrv.broker.put(True, "up2k.handle_json", body)
|
||||||
ret = x.get()
|
ret = x.get()
|
||||||
if sub:
|
|
||||||
ret["name"] = "/".join([sub, ret["name"]])
|
|
||||||
|
|
||||||
ret = json.dumps(ret)
|
ret = json.dumps(ret)
|
||||||
self.log(ret)
|
self.log(ret)
|
||||||
self.reply(ret.encode("utf-8"), mime="application/json")
|
self.reply(ret.encode("utf-8"), mime="application/json")
|
||||||
@@ -756,7 +874,7 @@ class HttpCli(object):
|
|||||||
times = (int(time.time()), int(lastmod))
|
times = (int(time.time()), int(lastmod))
|
||||||
self.log("no more chunks, setting times {}".format(times))
|
self.log("no more chunks, setting times {}".format(times))
|
||||||
try:
|
try:
|
||||||
os.utime(fsenc(path), times)
|
bos.utime(path, times)
|
||||||
except:
|
except:
|
||||||
self.log("failed to utime ({}, {})".format(path, times))
|
self.log("failed to utime ({}, {})".format(path, times))
|
||||||
|
|
||||||
@@ -770,12 +888,12 @@ class HttpCli(object):
|
|||||||
self.parser.drop()
|
self.parser.drop()
|
||||||
|
|
||||||
ck, msg = self.get_pwd_cookie(pwd)
|
ck, msg = self.get_pwd_cookie(pwd)
|
||||||
html = self.j2("msg", h1=msg, h2='<a href="/">ack</a>', redir="/")
|
html = self.j2("msg", h1=msg, h2='<a href="/?h">ack</a>', redir="/?h")
|
||||||
self.reply(html.encode("utf-8"), headers={"Set-Cookie": ck})
|
self.reply(html.encode("utf-8"), headers={"Set-Cookie": ck})
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def get_pwd_cookie(self, pwd):
|
def get_pwd_cookie(self, pwd):
|
||||||
if pwd in self.asrv.iuser:
|
if pwd in self.asrv.iacct:
|
||||||
msg = "login ok"
|
msg = "login ok"
|
||||||
dt = datetime.utcfromtimestamp(time.time() + 60 * 60 * 24 * 365)
|
dt = datetime.utcfromtimestamp(time.time() + 60 * 60 * 24 * 365)
|
||||||
exp = dt.strftime("%a, %d %b %Y %H:%M:%S GMT")
|
exp = dt.strftime("%a, %d %b %Y %H:%M:%S GMT")
|
||||||
@@ -795,20 +913,20 @@ class HttpCli(object):
|
|||||||
vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True)
|
vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True)
|
||||||
self._assert_safe_rem(rem)
|
self._assert_safe_rem(rem)
|
||||||
|
|
||||||
sanitized = sanitize_fn(new_dir)
|
sanitized = sanitize_fn(new_dir, "", [])
|
||||||
|
|
||||||
if not nullwrite:
|
if not nullwrite:
|
||||||
fdir = os.path.join(vfs.realpath, rem)
|
fdir = os.path.join(vfs.realpath, rem)
|
||||||
fn = os.path.join(fdir, sanitized)
|
fn = os.path.join(fdir, sanitized)
|
||||||
|
|
||||||
if not os.path.isdir(fsenc(fdir)):
|
if not bos.path.isdir(fdir):
|
||||||
raise Pebkac(500, "parent folder does not exist")
|
raise Pebkac(500, "parent folder does not exist")
|
||||||
|
|
||||||
if os.path.isdir(fsenc(fn)):
|
if bos.path.isdir(fn):
|
||||||
raise Pebkac(500, "that folder exists already")
|
raise Pebkac(500, "that folder exists already")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
os.mkdir(fsenc(fn))
|
bos.mkdir(fn)
|
||||||
except OSError as ex:
|
except OSError as ex:
|
||||||
if ex.errno == 13:
|
if ex.errno == 13:
|
||||||
raise Pebkac(500, "the server OS denied write-access")
|
raise Pebkac(500, "the server OS denied write-access")
|
||||||
@@ -832,13 +950,13 @@ class HttpCli(object):
|
|||||||
if not new_file.endswith(".md"):
|
if not new_file.endswith(".md"):
|
||||||
new_file += ".md"
|
new_file += ".md"
|
||||||
|
|
||||||
sanitized = sanitize_fn(new_file)
|
sanitized = sanitize_fn(new_file, "", [])
|
||||||
|
|
||||||
if not nullwrite:
|
if not nullwrite:
|
||||||
fdir = os.path.join(vfs.realpath, rem)
|
fdir = os.path.join(vfs.realpath, rem)
|
||||||
fn = os.path.join(fdir, sanitized)
|
fn = os.path.join(fdir, sanitized)
|
||||||
|
|
||||||
if os.path.exists(fsenc(fn)):
|
if bos.path.exists(fn):
|
||||||
raise Pebkac(500, "that file exists already")
|
raise Pebkac(500, "that file exists already")
|
||||||
|
|
||||||
with open(fsenc(fn), "wb") as f:
|
with open(fsenc(fn), "wb") as f:
|
||||||
@@ -853,6 +971,15 @@ class HttpCli(object):
|
|||||||
vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True)
|
vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True)
|
||||||
self._assert_safe_rem(rem)
|
self._assert_safe_rem(rem)
|
||||||
|
|
||||||
|
upload_vpath = self.vpath
|
||||||
|
lim = vfs.get_dbv(rem)[0].lim
|
||||||
|
fdir_base = os.path.join(vfs.realpath, rem)
|
||||||
|
if lim:
|
||||||
|
fdir_base, rem = lim.all(self.ip, rem, -1, fdir_base)
|
||||||
|
upload_vpath = "{}/{}".format(vfs.vpath, rem).strip("/")
|
||||||
|
if not nullwrite:
|
||||||
|
bos.makedirs(fdir_base)
|
||||||
|
|
||||||
files = []
|
files = []
|
||||||
errmsg = ""
|
errmsg = ""
|
||||||
t0 = time.time()
|
t0 = time.time()
|
||||||
@@ -862,13 +989,10 @@ class HttpCli(object):
|
|||||||
self.log("discarding incoming file without filename")
|
self.log("discarding incoming file without filename")
|
||||||
# fallthrough
|
# fallthrough
|
||||||
|
|
||||||
|
fdir = fdir_base
|
||||||
|
fname = sanitize_fn(p_file, "", [".prologue.html", ".epilogue.html"])
|
||||||
if p_file and not nullwrite:
|
if p_file and not nullwrite:
|
||||||
fdir = os.path.join(vfs.realpath, rem)
|
if not bos.path.isdir(fdir):
|
||||||
fname = sanitize_fn(
|
|
||||||
p_file, bad=[".prologue.html", ".epilogue.html"]
|
|
||||||
)
|
|
||||||
|
|
||||||
if not os.path.isdir(fsenc(fdir)):
|
|
||||||
raise Pebkac(404, "that folder does not exist")
|
raise Pebkac(404, "that folder does not exist")
|
||||||
|
|
||||||
suffix = ".{:.6f}-{}".format(time.time(), self.ip)
|
suffix = ".{:.6f}-{}".format(time.time(), self.ip)
|
||||||
@@ -878,14 +1002,28 @@ class HttpCli(object):
|
|||||||
fname = os.devnull
|
fname = os.devnull
|
||||||
fdir = ""
|
fdir = ""
|
||||||
|
|
||||||
|
if lim:
|
||||||
|
lim.chk_bup(self.ip)
|
||||||
|
lim.chk_nup(self.ip)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with ren_open(fname, "wb", 512 * 1024, **open_args) as f:
|
with ren_open(fname, "wb", 512 * 1024, **open_args) as f:
|
||||||
f, fname = f["orz"]
|
f, fname = f["orz"]
|
||||||
self.log("writing to {}/{}".format(fdir, fname))
|
abspath = os.path.join(fdir, fname)
|
||||||
|
self.log("writing to {}".format(abspath))
|
||||||
sz, sha512_hex, _ = hashcopy(p_data, f)
|
sz, sha512_hex, _ = hashcopy(p_data, f)
|
||||||
if sz == 0:
|
if sz == 0:
|
||||||
raise Pebkac(400, "empty files in post")
|
raise Pebkac(400, "empty files in post")
|
||||||
|
|
||||||
|
if lim:
|
||||||
|
lim.nup(self.ip)
|
||||||
|
lim.bup(self.ip, sz)
|
||||||
|
try:
|
||||||
|
lim.chk_sz(sz)
|
||||||
|
except:
|
||||||
|
bos.unlink(abspath)
|
||||||
|
raise
|
||||||
|
|
||||||
files.append([sz, sha512_hex, p_file, fname])
|
files.append([sz, sha512_hex, p_file, fname])
|
||||||
dbv, vrem = vfs.get_dbv(rem)
|
dbv, vrem = vfs.get_dbv(rem)
|
||||||
self.conn.hsrv.broker.put(
|
self.conn.hsrv.broker.put(
|
||||||
@@ -895,6 +1033,8 @@ class HttpCli(object):
|
|||||||
dbv.flags,
|
dbv.flags,
|
||||||
vrem,
|
vrem,
|
||||||
fname,
|
fname,
|
||||||
|
self.ip,
|
||||||
|
time.time(),
|
||||||
)
|
)
|
||||||
self.conn.nbyte += sz
|
self.conn.nbyte += sz
|
||||||
|
|
||||||
@@ -907,15 +1047,17 @@ class HttpCli(object):
|
|||||||
|
|
||||||
suffix = ".PARTIAL"
|
suffix = ".PARTIAL"
|
||||||
try:
|
try:
|
||||||
os.rename(fsenc(fp), fsenc(fp2 + suffix))
|
bos.rename(fp, fp2 + suffix)
|
||||||
except:
|
except:
|
||||||
fp2 = fp2[: -len(suffix) - 1]
|
fp2 = fp2[: -len(suffix) - 1]
|
||||||
os.rename(fsenc(fp), fsenc(fp2 + suffix))
|
bos.rename(fp, fp2 + suffix)
|
||||||
|
|
||||||
raise
|
raise
|
||||||
|
|
||||||
except Pebkac as ex:
|
except Pebkac as ex:
|
||||||
errmsg = unicode(ex)
|
errmsg = vol_san(
|
||||||
|
self.asrv.vfs.all_vols.values(), unicode(ex).encode("utf-8")
|
||||||
|
).decode("utf-8")
|
||||||
|
|
||||||
td = max(0.1, time.time() - t0)
|
td = max(0.1, time.time() - t0)
|
||||||
sz_total = sum(x[0] for x in files)
|
sz_total = sum(x[0] for x in files)
|
||||||
@@ -935,7 +1077,7 @@ class HttpCli(object):
|
|||||||
errmsg = "ERROR: " + errmsg
|
errmsg = "ERROR: " + errmsg
|
||||||
|
|
||||||
for sz, sha512, ofn, lfn in files:
|
for sz, sha512, ofn, lfn in files:
|
||||||
vpath = (self.vpath + "/" if self.vpath else "") + lfn
|
vpath = "{}/{}".format(upload_vpath, lfn).strip("/")
|
||||||
msg += 'sha512: {} // {} bytes // <a href="/{}">{}</a>\n'.format(
|
msg += 'sha512: {} // {} bytes // <a href="/{}">{}</a>\n'.format(
|
||||||
sha512[:56], sz, quotep(vpath), html_escape(ofn, crlf=True)
|
sha512[:56], sz, quotep(vpath), html_escape(ofn, crlf=True)
|
||||||
)
|
)
|
||||||
@@ -994,13 +1136,20 @@ class HttpCli(object):
|
|||||||
vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True)
|
vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True)
|
||||||
self._assert_safe_rem(rem)
|
self._assert_safe_rem(rem)
|
||||||
|
|
||||||
# TODO:
|
clen = int(self.headers.get("content-length", -1))
|
||||||
# the per-volume read/write permissions must be replaced with permission flags
|
if clen == -1:
|
||||||
# which would decide how to handle uploads to filenames which are taken,
|
raise Pebkac(411)
|
||||||
# current behavior of creating a new name is a good default for binary files
|
|
||||||
# but should also offer a flag to takeover the filename and rename the old one
|
rp, fn = vsplit(rem)
|
||||||
#
|
fp = os.path.join(vfs.realpath, rp)
|
||||||
# stopgap:
|
lim = vfs.get_dbv(rem)[0].lim
|
||||||
|
if lim:
|
||||||
|
fp, rp = lim.all(self.ip, rp, clen, fp)
|
||||||
|
bos.makedirs(fp)
|
||||||
|
|
||||||
|
fp = os.path.join(fp, fn)
|
||||||
|
rem = "{}/{}".format(rp, fn).strip("/")
|
||||||
|
|
||||||
if not rem.endswith(".md"):
|
if not rem.endswith(".md"):
|
||||||
raise Pebkac(400, "only markdown pls")
|
raise Pebkac(400, "only markdown pls")
|
||||||
|
|
||||||
@@ -1012,10 +1161,9 @@ class HttpCli(object):
|
|||||||
self.reply(response.encode("utf-8"))
|
self.reply(response.encode("utf-8"))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
fp = os.path.join(vfs.realpath, rem)
|
|
||||||
srv_lastmod = srv_lastmod3 = -1
|
srv_lastmod = srv_lastmod3 = -1
|
||||||
try:
|
try:
|
||||||
st = os.stat(fsenc(fp))
|
st = bos.stat(fp)
|
||||||
srv_lastmod = st.st_mtime
|
srv_lastmod = st.st_mtime
|
||||||
srv_lastmod3 = int(srv_lastmod * 1000)
|
srv_lastmod3 = int(srv_lastmod * 1000)
|
||||||
except OSError as ex:
|
except OSError as ex:
|
||||||
@@ -1051,14 +1199,13 @@ class HttpCli(object):
|
|||||||
self.reply(response.encode("utf-8"))
|
self.reply(response.encode("utf-8"))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# TODO another hack re: pending permissions rework
|
|
||||||
mdir, mfile = os.path.split(fp)
|
mdir, mfile = os.path.split(fp)
|
||||||
mfile2 = "{}.{:.3f}.md".format(mfile[:-3], srv_lastmod)
|
mfile2 = "{}.{:.3f}.md".format(mfile[:-3], srv_lastmod)
|
||||||
try:
|
try:
|
||||||
os.mkdir(fsenc(os.path.join(mdir, ".hist")))
|
bos.mkdir(os.path.join(mdir, ".hist"))
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
os.rename(fsenc(fp), fsenc(os.path.join(mdir, ".hist", mfile2)))
|
bos.rename(fp, os.path.join(mdir, ".hist", mfile2))
|
||||||
|
|
||||||
p_field, _, p_data = next(self.parser.gen)
|
p_field, _, p_data = next(self.parser.gen)
|
||||||
if p_field != "body":
|
if p_field != "body":
|
||||||
@@ -1067,7 +1214,16 @@ class HttpCli(object):
|
|||||||
with open(fsenc(fp), "wb", 512 * 1024) as f:
|
with open(fsenc(fp), "wb", 512 * 1024) as f:
|
||||||
sz, sha512, _ = hashcopy(p_data, f)
|
sz, sha512, _ = hashcopy(p_data, f)
|
||||||
|
|
||||||
new_lastmod = os.stat(fsenc(fp)).st_mtime
|
if lim:
|
||||||
|
lim.nup(self.ip)
|
||||||
|
lim.bup(self.ip, sz)
|
||||||
|
try:
|
||||||
|
lim.chk_sz(sz)
|
||||||
|
except:
|
||||||
|
bos.unlink(fp)
|
||||||
|
raise
|
||||||
|
|
||||||
|
new_lastmod = bos.stat(fp).st_mtime
|
||||||
new_lastmod3 = int(new_lastmod * 1000)
|
new_lastmod3 = int(new_lastmod * 1000)
|
||||||
sha512 = sha512[:56]
|
sha512 = sha512[:56]
|
||||||
|
|
||||||
@@ -1112,7 +1268,7 @@ class HttpCli(object):
|
|||||||
for ext in ["", ".gz", ".br"]:
|
for ext in ["", ".gz", ".br"]:
|
||||||
try:
|
try:
|
||||||
fs_path = req_path + ext
|
fs_path = req_path + ext
|
||||||
st = os.stat(fsenc(fs_path))
|
st = bos.stat(fs_path)
|
||||||
file_ts = max(file_ts, st.st_mtime)
|
file_ts = max(file_ts, st.st_mtime)
|
||||||
editions[ext or "plain"] = [fs_path, st.st_size]
|
editions[ext or "plain"] = [fs_path, st.st_size]
|
||||||
except:
|
except:
|
||||||
@@ -1289,11 +1445,9 @@ class HttpCli(object):
|
|||||||
else:
|
else:
|
||||||
fn = self.headers.get("host", "hey")
|
fn = self.headers.get("host", "hey")
|
||||||
|
|
||||||
afn = "".join(
|
safe = (string.ascii_letters + string.digits).replace("%", "")
|
||||||
[x if x in (string.ascii_letters + string.digits) else "_" for x in fn]
|
afn = "".join([x if x in safe.replace('"', "") else "_" for x in fn])
|
||||||
)
|
bascii = unicode(safe).encode("utf-8")
|
||||||
|
|
||||||
bascii = unicode(string.ascii_letters + string.digits).encode("utf-8")
|
|
||||||
ufn = fn.encode("utf-8", "xmlcharrefreplace")
|
ufn = fn.encode("utf-8", "xmlcharrefreplace")
|
||||||
if PY2:
|
if PY2:
|
||||||
ufn = [unicode(x) if x in bascii else "%{:02x}".format(ord(x)) for x in ufn]
|
ufn = [unicode(x) if x in bascii else "%{:02x}".format(ord(x)) for x in ufn]
|
||||||
@@ -1308,11 +1462,12 @@ class HttpCli(object):
|
|||||||
|
|
||||||
cdis = "attachment; filename=\"{}.{}\"; filename*=UTF-8''{}.{}"
|
cdis = "attachment; filename=\"{}.{}\"; filename*=UTF-8''{}.{}"
|
||||||
cdis = cdis.format(afn, fmt, ufn, fmt)
|
cdis = cdis.format(afn, fmt, ufn, fmt)
|
||||||
|
self.log(cdis)
|
||||||
self.send_headers(None, mime=mime, headers={"Content-Disposition": cdis})
|
self.send_headers(None, mime=mime, headers={"Content-Disposition": cdis})
|
||||||
|
|
||||||
fgen = vn.zipgen(rem, items, self.uname, dots, not self.args.no_scandir)
|
fgen = vn.zipgen(rem, items, self.uname, dots, not self.args.no_scandir)
|
||||||
# for f in fgen: print(repr({k: f[k] for k in ["vp", "ap"]}))
|
# for f in fgen: print(repr({k: f[k] for k in ["vp", "ap"]}))
|
||||||
bgen = packer(fgen, utf8="utf" in uarg, pre_crc="crc" in uarg)
|
bgen = packer(self.log, fgen, utf8="utf" in uarg, pre_crc="crc" in uarg)
|
||||||
bsent = 0
|
bsent = 0
|
||||||
for buf in bgen.gen():
|
for buf in bgen.gen():
|
||||||
if not buf:
|
if not buf:
|
||||||
@@ -1364,10 +1519,10 @@ class HttpCli(object):
|
|||||||
html_path = os.path.join(E.mod, "web", "{}.html".format(tpl))
|
html_path = os.path.join(E.mod, "web", "{}.html".format(tpl))
|
||||||
template = self.j2(tpl)
|
template = self.j2(tpl)
|
||||||
|
|
||||||
st = os.stat(fsenc(fs_path))
|
st = bos.stat(fs_path)
|
||||||
ts_md = st.st_mtime
|
ts_md = st.st_mtime
|
||||||
|
|
||||||
st = os.stat(fsenc(html_path))
|
st = bos.stat(html_path)
|
||||||
ts_html = st.st_mtime
|
ts_html = st.st_mtime
|
||||||
|
|
||||||
sz_md = 0
|
sz_md = 0
|
||||||
@@ -1376,7 +1531,7 @@ class HttpCli(object):
|
|||||||
for c, v in [[b"&", 4], [b"<", 3], [b">", 3]]:
|
for c, v in [[b"&", 4], [b"<", 3], [b">", 3]]:
|
||||||
sz_md += (len(buf) - len(buf.replace(c, b""))) * v
|
sz_md += (len(buf) - len(buf.replace(c, b""))) * v
|
||||||
|
|
||||||
file_ts = max(ts_md, ts_html)
|
file_ts = max(ts_md, ts_html, E.t0)
|
||||||
file_lastmod, do_send = self._chk_lastmod(file_ts)
|
file_lastmod, do_send = self._chk_lastmod(file_ts)
|
||||||
self.out_headers["Last-Modified"] = file_lastmod
|
self.out_headers["Last-Modified"] = file_lastmod
|
||||||
self.out_headers.update(NO_CACHE)
|
self.out_headers.update(NO_CACHE)
|
||||||
@@ -1423,13 +1578,14 @@ class HttpCli(object):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def tx_mounts(self):
|
def tx_mounts(self):
|
||||||
suf = self.urlq(rm=["h"])
|
suf = self.urlq({}, ["h"])
|
||||||
|
avol = [x for x in self.wvol if x in self.rvol]
|
||||||
rvol, wvol, avol = [
|
rvol, wvol, avol = [
|
||||||
[("/" + x).rstrip("/") + "/" for x in y]
|
[("/" + x).rstrip("/") + "/" for x in y]
|
||||||
for y in [self.rvol, self.wvol, self.avol]
|
for y in [self.rvol, self.wvol, avol]
|
||||||
]
|
]
|
||||||
|
|
||||||
if self.avol and not self.args.no_rescan:
|
if avol and not self.args.no_rescan:
|
||||||
x = self.conn.hsrv.broker.put(True, "up2k.get_state")
|
x = self.conn.hsrv.broker.put(True, "up2k.get_state")
|
||||||
vs = json.loads(x.get())
|
vs = json.loads(x.get())
|
||||||
vstate = {("/" + k).rstrip("/") + "/": v for k, v in vs["volstate"].items()}
|
vstate = {("/" + k).rstrip("/") + "/": v for k, v in vs["volstate"].items()}
|
||||||
@@ -1454,11 +1610,11 @@ class HttpCli(object):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def scanvol(self):
|
def scanvol(self):
|
||||||
if not self.readable or not self.writable:
|
if not self.can_read or not self.can_write:
|
||||||
raise Pebkac(403, "not admin")
|
raise Pebkac(403, "not allowed for user " + self.uname)
|
||||||
|
|
||||||
if self.args.no_rescan:
|
if self.args.no_rescan:
|
||||||
raise Pebkac(403, "disabled by argv")
|
raise Pebkac(403, "the rescan feature is disabled in server config")
|
||||||
|
|
||||||
vn, _ = self.asrv.vfs.get(self.vpath, self.uname, True, True)
|
vn, _ = self.asrv.vfs.get(self.vpath, self.uname, True, True)
|
||||||
|
|
||||||
@@ -1473,11 +1629,11 @@ class HttpCli(object):
|
|||||||
raise Pebkac(500, x)
|
raise Pebkac(500, x)
|
||||||
|
|
||||||
def tx_stack(self):
|
def tx_stack(self):
|
||||||
if not self.avol:
|
if not [x for x in self.wvol if x in self.rvol]:
|
||||||
raise Pebkac(403, "not admin")
|
raise Pebkac(403, "not allowed for user " + self.uname)
|
||||||
|
|
||||||
if self.args.no_stack:
|
if self.args.no_stack:
|
||||||
raise Pebkac(403, "disabled by argv")
|
raise Pebkac(403, "the stackdump feature is disabled in server config")
|
||||||
|
|
||||||
ret = "<pre>{}\n{}".format(time.time(), alltrace())
|
ret = "<pre>{}\n{}".format(time.time(), alltrace())
|
||||||
self.reply(ret.encode("utf-8"))
|
self.reply(ret.encode("utf-8"))
|
||||||
@@ -1512,7 +1668,7 @@ class HttpCli(object):
|
|||||||
try:
|
try:
|
||||||
vn, rem = self.asrv.vfs.get(top, self.uname, True, False)
|
vn, rem = self.asrv.vfs.get(top, self.uname, True, False)
|
||||||
fsroot, vfs_ls, vfs_virt = vn.ls(
|
fsroot, vfs_ls, vfs_virt = vn.ls(
|
||||||
rem, self.uname, not self.args.no_scandir, incl_wo=True
|
rem, self.uname, not self.args.no_scandir, [[True], [False, True]]
|
||||||
)
|
)
|
||||||
except:
|
except:
|
||||||
vfs_ls = []
|
vfs_ls = []
|
||||||
@@ -1539,6 +1695,74 @@ class HttpCli(object):
|
|||||||
ret["a"] = dirs
|
ret["a"] = dirs
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
def tx_ups(self):
|
||||||
|
if not self.args.unpost:
|
||||||
|
raise Pebkac(400, "the unpost feature is disabled in server config")
|
||||||
|
|
||||||
|
filt = self.uparam.get("filter")
|
||||||
|
lm = "ups [{}]".format(filt)
|
||||||
|
self.log(lm)
|
||||||
|
|
||||||
|
ret = []
|
||||||
|
t0 = time.time()
|
||||||
|
idx = self.conn.get_u2idx()
|
||||||
|
lim = time.time() - self.args.unpost
|
||||||
|
for vol in self.asrv.vfs.all_vols.values():
|
||||||
|
cur = idx.get_cur(vol.realpath)
|
||||||
|
if not cur:
|
||||||
|
continue
|
||||||
|
|
||||||
|
q = "select sz, rd, fn, at from up where ip=? and at>?"
|
||||||
|
for sz, rd, fn, at in cur.execute(q, (self.ip, lim)):
|
||||||
|
vp = "/" + "/".join(x for x in [vol.vpath, rd, fn] if x)
|
||||||
|
if filt and filt not in vp:
|
||||||
|
continue
|
||||||
|
|
||||||
|
ret.append({"vp": vp, "sz": sz, "at": at})
|
||||||
|
if len(ret) > 3000:
|
||||||
|
ret.sort(key=lambda x: x["at"], reverse=True)
|
||||||
|
ret = ret[:2000]
|
||||||
|
|
||||||
|
ret.sort(key=lambda x: x["at"], reverse=True)
|
||||||
|
ret = ret[:2000]
|
||||||
|
|
||||||
|
jtxt = json.dumps(ret, indent=2, sort_keys=True).encode("utf-8", "replace")
|
||||||
|
self.log("{} #{} {:.2f}sec".format(lm, len(ret), time.time() - t0))
|
||||||
|
self.reply(jtxt, mime="application/json")
|
||||||
|
|
||||||
|
def handle_rm(self, req=None):
|
||||||
|
if not req and not self.can_delete:
|
||||||
|
raise Pebkac(403, "not allowed for user " + self.uname)
|
||||||
|
|
||||||
|
if self.args.no_del:
|
||||||
|
raise Pebkac(403, "the delete feature is disabled in server config")
|
||||||
|
|
||||||
|
if not req:
|
||||||
|
req = [self.vpath]
|
||||||
|
|
||||||
|
x = self.conn.hsrv.broker.put(True, "up2k.handle_rm", self.uname, self.ip, req)
|
||||||
|
self.loud_reply(x.get())
|
||||||
|
|
||||||
|
def handle_mv(self):
|
||||||
|
if not self.can_move:
|
||||||
|
raise Pebkac(403, "not allowed for user " + self.uname)
|
||||||
|
|
||||||
|
if self.args.no_mv:
|
||||||
|
raise Pebkac(403, "the rename/move feature is disabled in server config")
|
||||||
|
|
||||||
|
# full path of new loc (incl filename)
|
||||||
|
dst = self.uparam.get("move")
|
||||||
|
if not dst:
|
||||||
|
raise Pebkac(400, "need dst vpath")
|
||||||
|
|
||||||
|
# x-www-form-urlencoded (url query part) uses
|
||||||
|
# either + or %20 for 0x20 so handle both
|
||||||
|
dst = unquotep(dst.replace("+", " "))
|
||||||
|
x = self.conn.hsrv.broker.put(
|
||||||
|
True, "up2k.handle_mv", self.uname, self.vpath, dst
|
||||||
|
)
|
||||||
|
self.loud_reply(x.get())
|
||||||
|
|
||||||
def tx_browser(self):
|
def tx_browser(self):
|
||||||
vpath = ""
|
vpath = ""
|
||||||
vpnodes = [["", "/"]]
|
vpnodes = [["", "/"]]
|
||||||
@@ -1551,18 +1775,16 @@ class HttpCli(object):
|
|||||||
|
|
||||||
vpnodes.append([quotep(vpath) + "/", html_escape(node, crlf=True)])
|
vpnodes.append([quotep(vpath) + "/", html_escape(node, crlf=True)])
|
||||||
|
|
||||||
vn, rem = self.asrv.vfs.get(
|
vn, rem = self.asrv.vfs.get(self.vpath, self.uname, False, False)
|
||||||
self.vpath, self.uname, self.readable, self.writable
|
|
||||||
)
|
|
||||||
abspath = vn.canonical(rem)
|
abspath = vn.canonical(rem)
|
||||||
dbv, vrem = vn.get_dbv(rem)
|
dbv, vrem = vn.get_dbv(rem)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
st = os.stat(fsenc(abspath))
|
st = bos.stat(abspath)
|
||||||
except:
|
except:
|
||||||
raise Pebkac(404)
|
raise Pebkac(404)
|
||||||
|
|
||||||
if self.readable:
|
if self.can_read:
|
||||||
if rem.startswith(".hist/up2k.") or (
|
if rem.startswith(".hist/up2k.") or (
|
||||||
rem.endswith("/dir.txt") and rem.startswith(".hist/th/")
|
rem.endswith("/dir.txt") and rem.startswith(".hist/th/")
|
||||||
):
|
):
|
||||||
@@ -1574,8 +1796,8 @@ class HttpCli(object):
|
|||||||
if is_dir:
|
if is_dir:
|
||||||
for fn in self.args.th_covers.split(","):
|
for fn in self.args.th_covers.split(","):
|
||||||
fp = os.path.join(abspath, fn)
|
fp = os.path.join(abspath, fn)
|
||||||
if os.path.exists(fp):
|
if bos.path.exists(fp):
|
||||||
vrem = "{}/{}".format(vrem.rstrip("/"), fn)
|
vrem = "{}/{}".format(vrem.rstrip("/"), fn).strip("/")
|
||||||
is_dir = False
|
is_dir = False
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -1629,12 +1851,16 @@ class HttpCli(object):
|
|||||||
srv_info = "</span> /// <span>".join(srv_info)
|
srv_info = "</span> /// <span>".join(srv_info)
|
||||||
|
|
||||||
perms = []
|
perms = []
|
||||||
if self.readable:
|
if self.can_read:
|
||||||
perms.append("read")
|
perms.append("read")
|
||||||
if self.writable:
|
if self.can_write:
|
||||||
perms.append("write")
|
perms.append("write")
|
||||||
|
if self.can_move:
|
||||||
|
perms.append("move")
|
||||||
|
if self.can_delete:
|
||||||
|
perms.append("delete")
|
||||||
|
|
||||||
url_suf = self.urlq()
|
url_suf = self.urlq({}, [])
|
||||||
is_ls = "ls" in self.uparam
|
is_ls = "ls" in self.uparam
|
||||||
|
|
||||||
tpl = "browser"
|
tpl = "browser"
|
||||||
@@ -1642,37 +1868,54 @@ class HttpCli(object):
|
|||||||
tpl = "browser2"
|
tpl = "browser2"
|
||||||
|
|
||||||
logues = ["", ""]
|
logues = ["", ""]
|
||||||
|
if not self.args.no_logues:
|
||||||
for n, fn in enumerate([".prologue.html", ".epilogue.html"]):
|
for n, fn in enumerate([".prologue.html", ".epilogue.html"]):
|
||||||
fn = os.path.join(abspath, fn)
|
fn = os.path.join(abspath, fn)
|
||||||
if os.path.exists(fsenc(fn)):
|
if bos.path.exists(fn):
|
||||||
with open(fsenc(fn), "rb") as f:
|
with open(fsenc(fn), "rb") as f:
|
||||||
logues[n] = f.read().decode("utf-8")
|
logues[n] = f.read().decode("utf-8")
|
||||||
|
|
||||||
|
readme = ""
|
||||||
|
if not self.args.no_readme and not logues[1]:
|
||||||
|
for fn in ["README.md", "readme.md"]:
|
||||||
|
fn = os.path.join(abspath, fn)
|
||||||
|
if bos.path.exists(fn):
|
||||||
|
with open(fsenc(fn), "rb") as f:
|
||||||
|
readme = f.read().decode("utf-8")
|
||||||
|
break
|
||||||
|
|
||||||
ls_ret = {
|
ls_ret = {
|
||||||
"dirs": [],
|
"dirs": [],
|
||||||
"files": [],
|
"files": [],
|
||||||
"taglist": [],
|
"taglist": [],
|
||||||
"srvinf": srv_info,
|
"srvinf": srv_info,
|
||||||
|
"acct": self.uname,
|
||||||
"perms": perms,
|
"perms": perms,
|
||||||
"logues": logues,
|
"logues": logues,
|
||||||
|
"readme": readme,
|
||||||
}
|
}
|
||||||
j2a = {
|
j2a = {
|
||||||
"vdir": quotep(self.vpath),
|
"vdir": quotep(self.vpath),
|
||||||
"vpnodes": vpnodes,
|
"vpnodes": vpnodes,
|
||||||
"files": [],
|
"files": [],
|
||||||
|
"acct": self.uname,
|
||||||
"perms": json.dumps(perms),
|
"perms": json.dumps(perms),
|
||||||
"taglist": [],
|
"taglist": [],
|
||||||
"tag_order": [],
|
"def_hcols": [],
|
||||||
"have_up2k_idx": ("e2d" in vn.flags),
|
"have_up2k_idx": ("e2d" in vn.flags),
|
||||||
"have_tags_idx": ("e2t" in vn.flags),
|
"have_tags_idx": ("e2t" in vn.flags),
|
||||||
|
"have_mv": (not self.args.no_mv),
|
||||||
|
"have_del": (not self.args.no_del),
|
||||||
"have_zip": (not self.args.no_zip),
|
"have_zip": (not self.args.no_zip),
|
||||||
"have_b_u": (self.writable and self.uparam.get("b") == "u"),
|
"have_unpost": (self.args.unpost > 0),
|
||||||
|
"have_b_u": (self.can_write and self.uparam.get("b") == "u"),
|
||||||
"url_suf": url_suf,
|
"url_suf": url_suf,
|
||||||
"logues": logues,
|
"logues": logues,
|
||||||
|
"readme": readme,
|
||||||
"title": html_escape(self.vpath, crlf=True),
|
"title": html_escape(self.vpath, crlf=True),
|
||||||
"srv_info": srv_info,
|
"srv_info": srv_info,
|
||||||
}
|
}
|
||||||
if not self.readable:
|
if not self.can_read:
|
||||||
if is_ls:
|
if is_ls:
|
||||||
ret = json.dumps(ls_ret)
|
ret = json.dumps(ls_ret)
|
||||||
self.reply(
|
self.reply(
|
||||||
@@ -1695,7 +1938,7 @@ class HttpCli(object):
|
|||||||
return self.tx_zip(k, v, vn, rem, [], self.args.ed)
|
return self.tx_zip(k, v, vn, rem, [], self.args.ed)
|
||||||
|
|
||||||
fsroot, vfs_ls, vfs_virt = vn.ls(
|
fsroot, vfs_ls, vfs_virt = vn.ls(
|
||||||
rem, self.uname, not self.args.no_scandir, incl_wo=True
|
rem, self.uname, not self.args.no_scandir, [[True], [False, True]]
|
||||||
)
|
)
|
||||||
stats = {k: v for k, v in vfs_ls}
|
stats = {k: v for k, v in vfs_ls}
|
||||||
vfs_ls = [x[0] for x in vfs_ls]
|
vfs_ls = [x[0] for x in vfs_ls]
|
||||||
@@ -1706,7 +1949,7 @@ class HttpCli(object):
|
|||||||
histdir = os.path.join(fsroot, ".hist")
|
histdir = os.path.join(fsroot, ".hist")
|
||||||
ptn = re.compile(r"(.*)\.([0-9]+\.[0-9]{3})(\.[^\.]+)$")
|
ptn = re.compile(r"(.*)\.([0-9]+\.[0-9]{3})(\.[^\.]+)$")
|
||||||
try:
|
try:
|
||||||
for hfn in os.listdir(histdir):
|
for hfn in bos.listdir(histdir):
|
||||||
m = ptn.match(hfn)
|
m = ptn.match(hfn)
|
||||||
if not m:
|
if not m:
|
||||||
continue
|
continue
|
||||||
@@ -1747,7 +1990,7 @@ class HttpCli(object):
|
|||||||
fspath = fsroot + "/" + fn
|
fspath = fsroot + "/" + fn
|
||||||
|
|
||||||
try:
|
try:
|
||||||
inf = stats.get(fn) or os.stat(fsenc(fspath))
|
inf = stats.get(fn) or bos.stat(fspath)
|
||||||
except:
|
except:
|
||||||
self.log("broken symlink: {}".format(repr(fspath)))
|
self.log("broken symlink: {}".format(repr(fspath)))
|
||||||
continue
|
continue
|
||||||
@@ -1856,8 +2099,8 @@ class HttpCli(object):
|
|||||||
j2a["logues"] = logues
|
j2a["logues"] = logues
|
||||||
j2a["taglist"] = taglist
|
j2a["taglist"] = taglist
|
||||||
|
|
||||||
if "mte" in vn.flags:
|
if "mth" in vn.flags:
|
||||||
j2a["tag_order"] = json.dumps(vn.flags["mte"].split(","))
|
j2a["def_hcols"] = vn.flags["mth"].split(",")
|
||||||
|
|
||||||
if self.args.css_browser:
|
if self.args.css_browser:
|
||||||
j2a["css"] = self.args.css_browser
|
j2a["css"] = self.args.css_browser
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ class HttpConn(object):
|
|||||||
|
|
||||||
self.args = hsrv.args
|
self.args = hsrv.args
|
||||||
self.asrv = hsrv.asrv
|
self.asrv = hsrv.asrv
|
||||||
self.is_mp = hsrv.is_mp
|
|
||||||
self.cert_path = hsrv.cert_path
|
self.cert_path = hsrv.cert_path
|
||||||
|
|
||||||
enth = HAVE_PIL and not self.args.no_thumb
|
enth = HAVE_PIL and not self.args.no_thumb
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ except ImportError:
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
from .__init__ import E, PY2, MACOS
|
from .__init__ import E, PY2, MACOS
|
||||||
from .util import spack, min_ex
|
from .util import spack, min_ex, start_stackmon, start_log_thrs
|
||||||
|
from .bos import bos
|
||||||
from .httpconn import HttpConn
|
from .httpconn import HttpConn
|
||||||
|
|
||||||
if PY2:
|
if PY2:
|
||||||
@@ -42,14 +43,14 @@ class HttpSrv(object):
|
|||||||
relying on MpSrv for performance (HttpSrv is just plain threads)
|
relying on MpSrv for performance (HttpSrv is just plain threads)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, broker, is_mp=False):
|
def __init__(self, broker, nid):
|
||||||
self.broker = broker
|
self.broker = broker
|
||||||
self.is_mp = is_mp
|
self.nid = nid
|
||||||
self.args = broker.args
|
self.args = broker.args
|
||||||
self.log = broker.log
|
self.log = broker.log
|
||||||
self.asrv = broker.asrv
|
self.asrv = broker.asrv
|
||||||
|
|
||||||
self.name = "httpsrv-i{:x}".format(os.getpid())
|
self.name = "httpsrv" + ("-n{}-i{:x}".format(nid, os.getpid()) if nid else "")
|
||||||
self.mutex = threading.Lock()
|
self.mutex = threading.Lock()
|
||||||
self.stopping = False
|
self.stopping = False
|
||||||
|
|
||||||
@@ -73,7 +74,7 @@ class HttpSrv(object):
|
|||||||
}
|
}
|
||||||
|
|
||||||
cert_path = os.path.join(E.cfg, "cert.pem")
|
cert_path = os.path.join(E.cfg, "cert.pem")
|
||||||
if os.path.exists(cert_path):
|
if bos.path.exists(cert_path):
|
||||||
self.cert_path = cert_path
|
self.cert_path = cert_path
|
||||||
else:
|
else:
|
||||||
self.cert_path = None
|
self.cert_path = None
|
||||||
@@ -81,10 +82,18 @@ class HttpSrv(object):
|
|||||||
if self.tp_q:
|
if self.tp_q:
|
||||||
self.start_threads(4)
|
self.start_threads(4)
|
||||||
|
|
||||||
t = threading.Thread(target=self.thr_scaler)
|
name = "httpsrv-scaler" + ("-{}".format(nid) if nid else "")
|
||||||
|
t = threading.Thread(target=self.thr_scaler, name=name)
|
||||||
t.daemon = True
|
t.daemon = True
|
||||||
t.start()
|
t.start()
|
||||||
|
|
||||||
|
if nid:
|
||||||
|
if self.args.stackmon:
|
||||||
|
start_stackmon(self.args.stackmon, nid)
|
||||||
|
|
||||||
|
if self.args.log_thrs:
|
||||||
|
start_log_thrs(self.log, self.args.log_thrs, nid)
|
||||||
|
|
||||||
def start_threads(self, n):
|
def start_threads(self, n):
|
||||||
self.tp_nthr += n
|
self.tp_nthr += n
|
||||||
if self.args.log_htp:
|
if self.args.log_htp:
|
||||||
@@ -93,7 +102,7 @@ class HttpSrv(object):
|
|||||||
for _ in range(n):
|
for _ in range(n):
|
||||||
thr = threading.Thread(
|
thr = threading.Thread(
|
||||||
target=self.thr_poolw,
|
target=self.thr_poolw,
|
||||||
name="httpsrv-poolw",
|
name=self.name + "-poolw",
|
||||||
)
|
)
|
||||||
thr.daemon = True
|
thr.daemon = True
|
||||||
thr.start()
|
thr.start()
|
||||||
@@ -115,9 +124,14 @@ class HttpSrv(object):
|
|||||||
self.stop_threads(4)
|
self.stop_threads(4)
|
||||||
|
|
||||||
def listen(self, sck, nlisteners):
|
def listen(self, sck, nlisteners):
|
||||||
|
ip, port = sck.getsockname()
|
||||||
self.srvs.append(sck)
|
self.srvs.append(sck)
|
||||||
self.nclimax = math.ceil(self.args.nc * 1.0 / nlisteners)
|
self.nclimax = math.ceil(self.args.nc * 1.0 / nlisteners)
|
||||||
t = threading.Thread(target=self.thr_listen, args=(sck,))
|
t = threading.Thread(
|
||||||
|
target=self.thr_listen,
|
||||||
|
args=(sck,),
|
||||||
|
name="httpsrv-n{}-listen-{}-{}".format(self.nid or "0", ip, port),
|
||||||
|
)
|
||||||
t.daemon = True
|
t.daemon = True
|
||||||
t.start()
|
t.start()
|
||||||
|
|
||||||
@@ -127,6 +141,7 @@ class HttpSrv(object):
|
|||||||
fno = srv_sck.fileno()
|
fno = srv_sck.fileno()
|
||||||
msg = "subscribed @ {}:{} f{}".format(ip, port, fno)
|
msg = "subscribed @ {}:{} f{}".format(ip, port, fno)
|
||||||
self.log(self.name, msg)
|
self.log(self.name, msg)
|
||||||
|
self.broker.put(False, "cb_httpsrv_up")
|
||||||
while not self.stopping:
|
while not self.stopping:
|
||||||
if self.args.log_conn:
|
if self.args.log_conn:
|
||||||
self.log(self.name, "|%sC-ncli" % ("-" * 1,), c="1;30")
|
self.log(self.name, "|%sC-ncli" % ("-" * 1,), c="1;30")
|
||||||
@@ -158,30 +173,31 @@ class HttpSrv(object):
|
|||||||
"""takes an incoming tcp connection and creates a thread to handle it"""
|
"""takes an incoming tcp connection and creates a thread to handle it"""
|
||||||
now = time.time()
|
now = time.time()
|
||||||
|
|
||||||
if self.tp_time and now - self.tp_time > 300:
|
if now - (self.tp_time or now) > 300:
|
||||||
|
m = "httpserver threadpool died: tpt {:.2f}, now {:.2f}, nthr {}, ncli {}"
|
||||||
|
self.log(self.name, m.format(self.tp_time, now, self.tp_nthr, self.ncli), 1)
|
||||||
|
self.tp_time = None
|
||||||
self.tp_q = None
|
self.tp_q = None
|
||||||
|
|
||||||
if self.tp_q:
|
|
||||||
self.tp_q.put((sck, addr))
|
|
||||||
with self.mutex:
|
with self.mutex:
|
||||||
self.ncli += 1
|
self.ncli += 1
|
||||||
|
if self.tp_q:
|
||||||
self.tp_time = self.tp_time or now
|
self.tp_time = self.tp_time or now
|
||||||
self.tp_ncli = max(self.tp_ncli, self.ncli + 1)
|
self.tp_ncli = max(self.tp_ncli, self.ncli)
|
||||||
if self.tp_nthr < self.ncli + 4:
|
if self.tp_nthr < self.ncli + 4:
|
||||||
self.start_threads(8)
|
self.start_threads(8)
|
||||||
|
|
||||||
|
self.tp_q.put((sck, addr))
|
||||||
return
|
return
|
||||||
|
|
||||||
if not self.args.no_htp:
|
if not self.args.no_htp:
|
||||||
m = "looks like the httpserver threadpool died; please make an issue on github and tell me the story of how you pulled that off, thanks and dog bless\n"
|
m = "looks like the httpserver threadpool died; please make an issue on github and tell me the story of how you pulled that off, thanks and dog bless\n"
|
||||||
self.log(self.name, m, 1)
|
self.log(self.name, m, 1)
|
||||||
|
|
||||||
with self.mutex:
|
|
||||||
self.ncli += 1
|
|
||||||
|
|
||||||
thr = threading.Thread(
|
thr = threading.Thread(
|
||||||
target=self.thr_client,
|
target=self.thr_client,
|
||||||
args=(sck, addr),
|
args=(sck, addr),
|
||||||
name="httpsrv-{}-{}".format(addr[0].split(".", 2)[-1][-6:], addr[1]),
|
name="httpconn-{}-{}".format(addr[0].split(".", 2)[-1][-6:], addr[1]),
|
||||||
)
|
)
|
||||||
thr.daemon = True
|
thr.daemon = True
|
||||||
thr.start()
|
thr.start()
|
||||||
@@ -198,11 +214,11 @@ class HttpSrv(object):
|
|||||||
try:
|
try:
|
||||||
sck, addr = task
|
sck, addr = task
|
||||||
me = threading.current_thread()
|
me = threading.current_thread()
|
||||||
me.name = (
|
me.name = "httpconn-{}-{}".format(
|
||||||
"httpsrv-{}-{}".format(addr[0].split(".", 2)[-1][-6:], addr[1]),
|
addr[0].split(".", 2)[-1][-6:], addr[1]
|
||||||
)
|
)
|
||||||
self.thr_client(sck, addr)
|
self.thr_client(sck, addr)
|
||||||
me.name = "httpsrv-poolw"
|
me.name = self.name + "-poolw"
|
||||||
except:
|
except:
|
||||||
self.log(self.name, "thr_client: " + min_ex(), 3)
|
self.log(self.name, "thr_client: " + min_ex(), 3)
|
||||||
|
|
||||||
@@ -228,7 +244,7 @@ class HttpSrv(object):
|
|||||||
if self.tp_q.empty():
|
if self.tp_q.empty():
|
||||||
break
|
break
|
||||||
|
|
||||||
self.log("httpsrv-i" + str(os.getpid()), "ok bye")
|
self.log(self.name, "ok bye")
|
||||||
|
|
||||||
def thr_client(self, sck, addr):
|
def thr_client(self, sck, addr):
|
||||||
"""thread managing one tcp client"""
|
"""thread managing one tcp client"""
|
||||||
@@ -294,7 +310,7 @@ class HttpSrv(object):
|
|||||||
try:
|
try:
|
||||||
with os.scandir(os.path.join(E.mod, "web")) as dh:
|
with os.scandir(os.path.join(E.mod, "web")) as dh:
|
||||||
for fh in dh:
|
for fh in dh:
|
||||||
inf = fh.stat(follow_symlinks=False)
|
inf = fh.stat()
|
||||||
v = max(v, inf.st_mtime)
|
v = max(v, inf.st_mtime)
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import subprocess as sp
|
|||||||
|
|
||||||
from .__init__ import PY2, WINDOWS, unicode
|
from .__init__ import PY2, WINDOWS, unicode
|
||||||
from .util import fsenc, fsdec, uncyg, REKOBO_LKEY
|
from .util import fsenc, fsdec, uncyg, REKOBO_LKEY
|
||||||
|
from .bos import bos
|
||||||
|
|
||||||
|
|
||||||
def have_ff(cmd):
|
def have_ff(cmd):
|
||||||
@@ -44,7 +45,7 @@ class MParser(object):
|
|||||||
if WINDOWS:
|
if WINDOWS:
|
||||||
bp = uncyg(bp)
|
bp = uncyg(bp)
|
||||||
|
|
||||||
if os.path.exists(bp):
|
if bos.path.exists(bp):
|
||||||
self.bin = bp
|
self.bin = bp
|
||||||
return
|
return
|
||||||
except:
|
except:
|
||||||
@@ -227,37 +228,47 @@ def parse_ffprobe(txt):
|
|||||||
class MTag(object):
|
class MTag(object):
|
||||||
def __init__(self, log_func, args):
|
def __init__(self, log_func, args):
|
||||||
self.log_func = log_func
|
self.log_func = log_func
|
||||||
|
self.args = args
|
||||||
self.usable = True
|
self.usable = True
|
||||||
self.prefer_mt = False
|
self.prefer_mt = not args.no_mtag_ff
|
||||||
mappings = args.mtm
|
|
||||||
self.backend = "ffprobe" if args.no_mutagen else "mutagen"
|
self.backend = "ffprobe" if args.no_mutagen else "mutagen"
|
||||||
or_ffprobe = " or ffprobe"
|
self.can_ffprobe = (
|
||||||
|
HAVE_FFPROBE
|
||||||
|
and not args.no_mtag_ff
|
||||||
|
and (not WINDOWS or sys.version_info >= (3, 8))
|
||||||
|
)
|
||||||
|
mappings = args.mtm
|
||||||
|
or_ffprobe = " or FFprobe"
|
||||||
|
|
||||||
if self.backend == "mutagen":
|
if self.backend == "mutagen":
|
||||||
self.get = self.get_mutagen
|
self.get = self.get_mutagen
|
||||||
try:
|
try:
|
||||||
import mutagen
|
import mutagen
|
||||||
except:
|
except:
|
||||||
self.log("could not load mutagen, trying ffprobe instead", c=3)
|
self.log("could not load Mutagen, trying FFprobe instead", c=3)
|
||||||
self.backend = "ffprobe"
|
self.backend = "ffprobe"
|
||||||
|
|
||||||
if self.backend == "ffprobe":
|
if self.backend == "ffprobe":
|
||||||
|
self.usable = self.can_ffprobe
|
||||||
self.get = self.get_ffprobe
|
self.get = self.get_ffprobe
|
||||||
self.prefer_mt = True
|
self.prefer_mt = True
|
||||||
# about 20x slower
|
|
||||||
self.usable = HAVE_FFPROBE
|
|
||||||
|
|
||||||
if self.usable and WINDOWS and sys.version_info < (3, 8):
|
if not HAVE_FFPROBE:
|
||||||
self.usable = False
|
pass
|
||||||
|
|
||||||
|
elif args.no_mtag_ff:
|
||||||
|
msg = "found FFprobe but it was disabled by --no-mtag-ff"
|
||||||
|
self.log(msg, c=3)
|
||||||
|
|
||||||
|
elif WINDOWS and sys.version_info < (3, 8):
|
||||||
or_ffprobe = " or python >= 3.8"
|
or_ffprobe = " or python >= 3.8"
|
||||||
msg = "found ffprobe but your python is too old; need 3.8 or newer"
|
msg = "found FFprobe but your python is too old; need 3.8 or newer"
|
||||||
self.log(msg, c=1)
|
self.log(msg, c=1)
|
||||||
|
|
||||||
if not self.usable:
|
if not self.usable:
|
||||||
msg = "need mutagen{} to read media tags so please run this:\n{}{} -m pip install --user mutagen\n"
|
msg = "need Mutagen{} to read media tags so please run this:\n{}{} -m pip install --user mutagen\n"
|
||||||
self.log(
|
pybin = os.path.basename(sys.executable)
|
||||||
msg.format(or_ffprobe, " " * 37, os.path.basename(sys.executable)), c=1
|
self.log(msg.format(or_ffprobe, " " * 37, pybin), c=1)
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# https://picard-docs.musicbrainz.org/downloads/MusicBrainz_Picard_Tag_Map.html
|
# https://picard-docs.musicbrainz.org/downloads/MusicBrainz_Picard_Tag_Map.html
|
||||||
@@ -387,7 +398,7 @@ class MTag(object):
|
|||||||
v2 = r2.get(k)
|
v2 = r2.get(k)
|
||||||
if v1 == v2:
|
if v1 == v2:
|
||||||
print(" ", k, v1)
|
print(" ", k, v1)
|
||||||
elif v1 != "0000": # ffprobe date=0
|
elif v1 != "0000": # FFprobe date=0
|
||||||
diffs.append(k)
|
diffs.append(k)
|
||||||
print(" 1", k, v1)
|
print(" 1", k, v1)
|
||||||
print(" 2", k, v2)
|
print(" 2", k, v2)
|
||||||
@@ -408,20 +419,41 @@ class MTag(object):
|
|||||||
md = mutagen.File(fsenc(abspath), easy=True)
|
md = mutagen.File(fsenc(abspath), easy=True)
|
||||||
x = md.info.length
|
x = md.info.length
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
return {}
|
return self.get_ffprobe(abspath) if self.can_ffprobe else {}
|
||||||
|
|
||||||
ret = {}
|
sz = bos.path.getsize(abspath)
|
||||||
try:
|
ret = {".q": [0, int((sz / md.info.length) / 128)]}
|
||||||
dur = int(md.info.length)
|
|
||||||
try:
|
|
||||||
q = int(md.info.bitrate / 1024)
|
|
||||||
except:
|
|
||||||
q = int((os.path.getsize(fsenc(abspath)) / dur) / 128)
|
|
||||||
|
|
||||||
ret[".dur"] = [0, dur]
|
for attr, k, norm in [
|
||||||
ret[".q"] = [0, q]
|
["codec", "ac", unicode],
|
||||||
|
["channels", "chs", int],
|
||||||
|
["sample_rate", ".hz", int],
|
||||||
|
["bitrate", ".aq", int],
|
||||||
|
["length", ".dur", int],
|
||||||
|
]:
|
||||||
|
try:
|
||||||
|
v = getattr(md.info, attr)
|
||||||
except:
|
except:
|
||||||
pass
|
if k != "ac":
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
v = str(md.info).split(".")[1]
|
||||||
|
if v.startswith("ogg"):
|
||||||
|
v = v[3:]
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not v:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if k == ".aq":
|
||||||
|
v /= 1000
|
||||||
|
|
||||||
|
if k == "ac" and v.startswith("mp4a.40."):
|
||||||
|
v = "aac"
|
||||||
|
|
||||||
|
ret[k] = [0, norm(v)]
|
||||||
|
|
||||||
return self.normalize_tags(ret, md)
|
return self.normalize_tags(ret, md)
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
from __future__ import print_function, unicode_literals
|
from __future__ import print_function, unicode_literals
|
||||||
|
|
||||||
import os
|
|
||||||
import tarfile
|
import tarfile
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
from .sutil import errdesc
|
from .sutil import errdesc
|
||||||
from .util import Queue, fsenc
|
from .util import Queue, fsenc
|
||||||
|
from .bos import bos
|
||||||
|
|
||||||
|
|
||||||
class QFile(object):
|
class QFile(object):
|
||||||
@@ -33,10 +33,11 @@ class QFile(object):
|
|||||||
class StreamTar(object):
|
class StreamTar(object):
|
||||||
"""construct in-memory tar file from the given path"""
|
"""construct in-memory tar file from the given path"""
|
||||||
|
|
||||||
def __init__(self, fgen, **kwargs):
|
def __init__(self, log, fgen, **kwargs):
|
||||||
self.ci = 0
|
self.ci = 0
|
||||||
self.co = 0
|
self.co = 0
|
||||||
self.qfile = QFile()
|
self.qfile = QFile()
|
||||||
|
self.log = log
|
||||||
self.fgen = fgen
|
self.fgen = fgen
|
||||||
self.errf = None
|
self.errf = None
|
||||||
|
|
||||||
@@ -60,7 +61,7 @@ class StreamTar(object):
|
|||||||
|
|
||||||
yield None
|
yield None
|
||||||
if self.errf:
|
if self.errf:
|
||||||
os.unlink(self.errf["ap"])
|
bos.unlink(self.errf["ap"])
|
||||||
|
|
||||||
def ser(self, f):
|
def ser(self, f):
|
||||||
name = f["vp"]
|
name = f["vp"]
|
||||||
@@ -91,7 +92,8 @@ class StreamTar(object):
|
|||||||
errors.append([f["vp"], repr(ex)])
|
errors.append([f["vp"], repr(ex)])
|
||||||
|
|
||||||
if errors:
|
if errors:
|
||||||
self.errf = errdesc(errors)
|
self.errf, txt = errdesc(errors)
|
||||||
|
self.log("\n".join(([repr(self.errf)] + txt[1:])))
|
||||||
self.ser(self.errf)
|
self.ser(self.errf)
|
||||||
|
|
||||||
self.tar.close()
|
self.tar.close()
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
|
||||||
"""
|
"""
|
||||||
This is Victor Stinner's pure-Python implementation of PEP 383: the "surrogateescape" error
|
This is Victor Stinner's pure-Python implementation of PEP 383: the "surrogateescape" error
|
||||||
handler of Python 3.
|
handler of Python 3.
|
||||||
@@ -171,7 +173,7 @@ FS_ENCODING = sys.getfilesystemencoding()
|
|||||||
|
|
||||||
if WINDOWS and not PY3:
|
if WINDOWS and not PY3:
|
||||||
# py2 thinks win* is mbcs, probably a bug? anyways this works
|
# py2 thinks win* is mbcs, probably a bug? anyways this works
|
||||||
FS_ENCODING = 'utf-8'
|
FS_ENCODING = "utf-8"
|
||||||
|
|
||||||
|
|
||||||
# normalize the filesystem encoding name.
|
# normalize the filesystem encoding name.
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
from __future__ import print_function, unicode_literals
|
from __future__ import print_function, unicode_literals
|
||||||
|
|
||||||
import os
|
|
||||||
import time
|
import time
|
||||||
import tempfile
|
import tempfile
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
from .bos import bos
|
||||||
|
|
||||||
|
|
||||||
def errdesc(errors):
|
def errdesc(errors):
|
||||||
report = ["copyparty failed to add the following files to the archive:", ""]
|
report = ["copyparty failed to add the following files to the archive:", ""]
|
||||||
@@ -17,12 +18,11 @@ def errdesc(errors):
|
|||||||
tf_path = tf.name
|
tf_path = tf.name
|
||||||
tf.write("\r\n".join(report).encode("utf-8", "replace"))
|
tf.write("\r\n".join(report).encode("utf-8", "replace"))
|
||||||
|
|
||||||
dt = datetime.utcfromtimestamp(time.time())
|
dt = datetime.utcnow().strftime("%Y-%m%d-%H%M%S")
|
||||||
dt = dt.strftime("%Y-%m%d-%H%M%S")
|
|
||||||
|
|
||||||
os.chmod(tf_path, 0o444)
|
bos.chmod(tf_path, 0o444)
|
||||||
return {
|
return {
|
||||||
"vp": "archive-errors-{}.txt".format(dt),
|
"vp": "archive-errors-{}.txt".format(dt),
|
||||||
"ap": tf_path,
|
"ap": tf_path,
|
||||||
"st": os.stat(tf_path),
|
"st": bos.stat(tf_path),
|
||||||
}
|
}, report
|
||||||
|
|||||||
@@ -6,12 +6,15 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import shlex
|
import shlex
|
||||||
|
import string
|
||||||
|
import signal
|
||||||
|
import socket
|
||||||
import threading
|
import threading
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import calendar
|
import calendar
|
||||||
|
|
||||||
from .__init__ import E, PY2, WINDOWS, MACOS, VT100
|
from .__init__ import E, PY2, WINDOWS, ANYWIN, MACOS, VT100, unicode
|
||||||
from .util import mp
|
from .util import mp, start_log_thrs, start_stackmon, min_ex, ansi_re
|
||||||
from .authsrv import AuthSrv
|
from .authsrv import AuthSrv
|
||||||
from .tcpsrv import TcpSrv
|
from .tcpsrv import TcpSrv
|
||||||
from .up2k import Up2k
|
from .up2k import Up2k
|
||||||
@@ -33,8 +36,11 @@ class SvcHub(object):
|
|||||||
self.args = args
|
self.args = args
|
||||||
self.argv = argv
|
self.argv = argv
|
||||||
self.logf = None
|
self.logf = None
|
||||||
|
self.stop_req = False
|
||||||
|
self.stopping = False
|
||||||
|
self.stop_cond = threading.Condition()
|
||||||
|
self.httpsrv_up = 0
|
||||||
|
|
||||||
self.ansi_re = re.compile("\033\\[[^m]*m")
|
|
||||||
self.log_mutex = threading.Lock()
|
self.log_mutex = threading.Lock()
|
||||||
self.next_day = 0
|
self.next_day = 0
|
||||||
|
|
||||||
@@ -42,8 +48,14 @@ class SvcHub(object):
|
|||||||
if args.lo:
|
if args.lo:
|
||||||
self._setup_logfile(printed)
|
self._setup_logfile(printed)
|
||||||
|
|
||||||
|
if args.stackmon:
|
||||||
|
start_stackmon(args.stackmon, 0)
|
||||||
|
|
||||||
|
if args.log_thrs:
|
||||||
|
start_log_thrs(self.log, args.log_thrs, 0)
|
||||||
|
|
||||||
# initiate all services to manage
|
# initiate all services to manage
|
||||||
self.asrv = AuthSrv(self.args, self.log, False)
|
self.asrv = AuthSrv(self.args, self.log)
|
||||||
if args.ls:
|
if args.ls:
|
||||||
self.asrv.dbg_ls()
|
self.asrv.dbg_ls()
|
||||||
|
|
||||||
@@ -74,8 +86,31 @@ class SvcHub(object):
|
|||||||
|
|
||||||
self.broker = Broker(self)
|
self.broker = Broker(self)
|
||||||
|
|
||||||
|
def thr_httpsrv_up(self):
|
||||||
|
time.sleep(5)
|
||||||
|
failed = self.broker.num_workers - self.httpsrv_up
|
||||||
|
if not failed:
|
||||||
|
return
|
||||||
|
|
||||||
|
m = "{}/{} workers failed to start"
|
||||||
|
m = m.format(failed, self.broker.num_workers)
|
||||||
|
self.log("root", m, 1)
|
||||||
|
os._exit(1)
|
||||||
|
|
||||||
|
def cb_httpsrv_up(self):
|
||||||
|
self.httpsrv_up += 1
|
||||||
|
if self.httpsrv_up != self.broker.num_workers:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.log("root", "workers OK\n")
|
||||||
|
self.up2k.init_vols()
|
||||||
|
|
||||||
|
thr = threading.Thread(target=self.sd_notify, name="sd-notify")
|
||||||
|
thr.daemon = True
|
||||||
|
thr.start()
|
||||||
|
|
||||||
def _logname(self):
|
def _logname(self):
|
||||||
dt = datetime.utcfromtimestamp(time.time())
|
dt = datetime.utcnow()
|
||||||
fn = self.args.lo
|
fn = self.args.lo
|
||||||
for fs in "YmdHMS":
|
for fs in "YmdHMS":
|
||||||
fs = "%" + fs
|
fs = "%" + fs
|
||||||
@@ -121,21 +156,68 @@ class SvcHub(object):
|
|||||||
print(msg, end="")
|
print(msg, end="")
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
thr = threading.Thread(target=self.tcpsrv.run, name="svchub-main")
|
self.tcpsrv.run()
|
||||||
|
|
||||||
|
thr = threading.Thread(target=self.thr_httpsrv_up)
|
||||||
thr.daemon = True
|
thr.daemon = True
|
||||||
thr.start()
|
thr.start()
|
||||||
|
|
||||||
# winxp/py2.7 support: thr.join() kills signals
|
for sig in [signal.SIGINT, signal.SIGTERM]:
|
||||||
try:
|
signal.signal(sig, self.signal_handler)
|
||||||
while True:
|
|
||||||
time.sleep(9001)
|
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
# macos hangs after shutdown on sigterm with while-sleep,
|
||||||
|
# windows cannot ^c stop_cond (and win10 does the macos thing but winxp is fine??)
|
||||||
|
# linux is fine with both,
|
||||||
|
# never lucky
|
||||||
|
if ANYWIN:
|
||||||
|
# msys-python probably fine but >msys-python
|
||||||
|
thr = threading.Thread(target=self.stop_thr, name="svchub-sig")
|
||||||
|
thr.daemon = True
|
||||||
|
thr.start()
|
||||||
|
|
||||||
|
try:
|
||||||
|
while not self.stop_req:
|
||||||
|
time.sleep(1)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.shutdown()
|
||||||
|
thr.join()
|
||||||
|
else:
|
||||||
|
self.stop_thr()
|
||||||
|
|
||||||
|
def stop_thr(self):
|
||||||
|
while not self.stop_req:
|
||||||
|
with self.stop_cond:
|
||||||
|
self.stop_cond.wait(9001)
|
||||||
|
|
||||||
|
self.shutdown()
|
||||||
|
|
||||||
|
def signal_handler(self, sig, frame):
|
||||||
|
if self.stopping:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.stop_req = True
|
||||||
|
with self.stop_cond:
|
||||||
|
self.stop_cond.notify_all()
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
if self.stopping:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.stopping = True
|
||||||
|
self.stop_req = True
|
||||||
|
with self.stop_cond:
|
||||||
|
self.stop_cond.notify_all()
|
||||||
|
|
||||||
|
ret = 1
|
||||||
|
try:
|
||||||
with self.log_mutex:
|
with self.log_mutex:
|
||||||
print("OPYTHAT")
|
print("OPYTHAT")
|
||||||
|
|
||||||
self.tcpsrv.shutdown()
|
self.tcpsrv.shutdown()
|
||||||
self.broker.shutdown()
|
self.broker.shutdown()
|
||||||
|
self.up2k.shutdown()
|
||||||
if self.thumbsrv:
|
if self.thumbsrv:
|
||||||
self.thumbsrv.shutdown()
|
self.thumbsrv.shutdown()
|
||||||
|
|
||||||
@@ -148,18 +230,20 @@ class SvcHub(object):
|
|||||||
print("waiting for thumbsrv (10sec)...")
|
print("waiting for thumbsrv (10sec)...")
|
||||||
|
|
||||||
print("nailed it", end="")
|
print("nailed it", end="")
|
||||||
|
ret = 0
|
||||||
finally:
|
finally:
|
||||||
print("\033[0m")
|
print("\033[0m")
|
||||||
if self.logf:
|
if self.logf:
|
||||||
self.logf.close()
|
self.logf.close()
|
||||||
|
|
||||||
|
sys.exit(ret)
|
||||||
|
|
||||||
def _log_disabled(self, src, msg, c=0):
|
def _log_disabled(self, src, msg, c=0):
|
||||||
if not self.logf:
|
if not self.logf:
|
||||||
return
|
return
|
||||||
|
|
||||||
with self.log_mutex:
|
with self.log_mutex:
|
||||||
ts = datetime.utcfromtimestamp(time.time())
|
ts = datetime.utcnow().strftime("%Y-%m%d-%H%M%S.%f")[:-3]
|
||||||
ts = ts.strftime("%Y-%m%d-%H%M%S.%f")[:-3]
|
|
||||||
self.logf.write("@{} [{}] {}\n".format(ts, src, msg))
|
self.logf.write("@{} [{}] {}\n".format(ts, src, msg))
|
||||||
|
|
||||||
now = time.time()
|
now = time.time()
|
||||||
@@ -171,7 +255,7 @@ class SvcHub(object):
|
|||||||
self.logf.close()
|
self.logf.close()
|
||||||
self._setup_logfile("")
|
self._setup_logfile("")
|
||||||
|
|
||||||
dt = datetime.utcfromtimestamp(time.time())
|
dt = datetime.utcnow()
|
||||||
|
|
||||||
# unix timestamp of next 00:00:00 (leap-seconds safe)
|
# unix timestamp of next 00:00:00 (leap-seconds safe)
|
||||||
day_now = dt.day
|
day_now = dt.day
|
||||||
@@ -194,9 +278,9 @@ class SvcHub(object):
|
|||||||
if not VT100:
|
if not VT100:
|
||||||
fmt = "{} {:21} {}\n"
|
fmt = "{} {:21} {}\n"
|
||||||
if "\033" in msg:
|
if "\033" in msg:
|
||||||
msg = self.ansi_re.sub("", msg)
|
msg = ansi_re.sub("", msg)
|
||||||
if "\033" in src:
|
if "\033" in src:
|
||||||
src = self.ansi_re.sub("", src)
|
src = ansi_re.sub("", src)
|
||||||
elif c:
|
elif c:
|
||||||
if isinstance(c, int):
|
if isinstance(c, int):
|
||||||
msg = "\033[3{}m{}".format(c, msg)
|
msg = "\033[3{}m{}".format(c, msg)
|
||||||
@@ -262,3 +346,22 @@ class SvcHub(object):
|
|||||||
else:
|
else:
|
||||||
self.log("svchub", err)
|
self.log("svchub", err)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def sd_notify(self):
|
||||||
|
try:
|
||||||
|
addr = os.getenv("NOTIFY_SOCKET")
|
||||||
|
if not addr:
|
||||||
|
return
|
||||||
|
|
||||||
|
addr = unicode(addr)
|
||||||
|
if addr.startswith("@"):
|
||||||
|
addr = "\0" + addr[1:]
|
||||||
|
|
||||||
|
m = "".join(x for x in addr if x in string.printable)
|
||||||
|
self.log("sd_notify", m)
|
||||||
|
|
||||||
|
sck = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
|
||||||
|
sck.connect(addr)
|
||||||
|
sck.sendall(b"READY=1")
|
||||||
|
except:
|
||||||
|
self.log("sd_notify", min_ex())
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from datetime import datetime
|
|||||||
|
|
||||||
from .sutil import errdesc
|
from .sutil import errdesc
|
||||||
from .util import yieldfile, sanitize_fn, spack, sunpack
|
from .util import yieldfile, sanitize_fn, spack, sunpack
|
||||||
|
from .bos import bos
|
||||||
|
|
||||||
|
|
||||||
def dostime2unix(buf):
|
def dostime2unix(buf):
|
||||||
@@ -89,7 +90,7 @@ def gen_hdr(h_pos, fn, sz, lastmod, utf8, crc32, pre_crc):
|
|||||||
ret += spack(b"<LL", vsz, vsz)
|
ret += spack(b"<LL", vsz, vsz)
|
||||||
|
|
||||||
# windows support (the "?" replace below too)
|
# windows support (the "?" replace below too)
|
||||||
fn = sanitize_fn(fn, ok="/")
|
fn = sanitize_fn(fn, "/", [])
|
||||||
bfn = fn.encode("utf-8" if utf8 else "cp437", "replace").replace(b"?", b"_")
|
bfn = fn.encode("utf-8" if utf8 else "cp437", "replace").replace(b"?", b"_")
|
||||||
|
|
||||||
z64_len = len(z64v) * 8 + 4 if z64v else 0
|
z64_len = len(z64v) * 8 + 4 if z64v else 0
|
||||||
@@ -183,7 +184,8 @@ def gen_ecdr64_loc(ecdr64_pos):
|
|||||||
|
|
||||||
|
|
||||||
class StreamZip(object):
|
class StreamZip(object):
|
||||||
def __init__(self, fgen, utf8=False, pre_crc=False):
|
def __init__(self, log, fgen, utf8=False, pre_crc=False):
|
||||||
|
self.log = log
|
||||||
self.fgen = fgen
|
self.fgen = fgen
|
||||||
self.utf8 = utf8
|
self.utf8 = utf8
|
||||||
self.pre_crc = pre_crc
|
self.pre_crc = pre_crc
|
||||||
@@ -246,8 +248,8 @@ class StreamZip(object):
|
|||||||
errors.append([f["vp"], repr(ex)])
|
errors.append([f["vp"], repr(ex)])
|
||||||
|
|
||||||
if errors:
|
if errors:
|
||||||
errf = errdesc(errors)
|
errf, txt = errdesc(errors)
|
||||||
print(repr(errf))
|
self.log("\n".join(([repr(errf)] + txt[1:])))
|
||||||
for x in self.ser(errf):
|
for x in self.ser(errf):
|
||||||
yield x
|
yield x
|
||||||
|
|
||||||
@@ -270,4 +272,4 @@ class StreamZip(object):
|
|||||||
yield self._ct(ecdr)
|
yield self._ct(ecdr)
|
||||||
|
|
||||||
if errors:
|
if errors:
|
||||||
os.unlink(errf["ap"])
|
bos.unlink(errf["ap"])
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from __future__ import print_function, unicode_literals
|
|||||||
import re
|
import re
|
||||||
import socket
|
import socket
|
||||||
|
|
||||||
|
from .__init__ import MACOS, ANYWIN
|
||||||
from .util import chkcmd
|
from .util import chkcmd
|
||||||
|
|
||||||
|
|
||||||
@@ -29,14 +30,16 @@ class TcpSrv(object):
|
|||||||
for x in nonlocals:
|
for x in nonlocals:
|
||||||
eps[x] = "external"
|
eps[x] = "external"
|
||||||
|
|
||||||
|
msgs = []
|
||||||
|
m = "available @ http://{}:{}/ (\033[33m{}\033[0m)"
|
||||||
for ip, desc in sorted(eps.items(), key=lambda x: x[1]):
|
for ip, desc in sorted(eps.items(), key=lambda x: x[1]):
|
||||||
for port in sorted(self.args.p):
|
for port in sorted(self.args.p):
|
||||||
self.log(
|
msgs.append(m.format(ip, port, desc))
|
||||||
"tcpsrv",
|
|
||||||
"available @ http://{}:{}/ (\033[33m{}\033[0m)".format(
|
if msgs:
|
||||||
ip, port, desc
|
msgs[-1] += "\n"
|
||||||
),
|
for m in msgs:
|
||||||
)
|
self.log("tcpsrv", m)
|
||||||
|
|
||||||
self.srv = []
|
self.srv = []
|
||||||
for ip in self.args.i:
|
for ip in self.args.i:
|
||||||
@@ -81,26 +84,101 @@ class TcpSrv(object):
|
|||||||
|
|
||||||
self.log("tcpsrv", "ok bye")
|
self.log("tcpsrv", "ok bye")
|
||||||
|
|
||||||
def detect_interfaces(self, listen_ips):
|
def ips_linux(self):
|
||||||
eps = {}
|
eps = {}
|
||||||
|
|
||||||
# get all ips and their interfaces
|
|
||||||
try:
|
try:
|
||||||
ip_addr, _ = chkcmd("ip", "addr")
|
txt, _ = chkcmd(["ip", "addr"])
|
||||||
except:
|
except:
|
||||||
ip_addr = None
|
return eps
|
||||||
|
|
||||||
if ip_addr:
|
|
||||||
r = re.compile(r"^\s+inet ([^ ]+)/.* (.*)")
|
r = re.compile(r"^\s+inet ([^ ]+)/.* (.*)")
|
||||||
for ln in ip_addr.split("\n"):
|
for ln in txt.split("\n"):
|
||||||
try:
|
try:
|
||||||
ip, dev = r.match(ln.rstrip()).groups()
|
ip, dev = r.match(ln.rstrip()).groups()
|
||||||
for lip in listen_ips:
|
|
||||||
if lip in ["0.0.0.0", ip]:
|
|
||||||
eps[ip] = dev
|
eps[ip] = dev
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
return eps
|
||||||
|
|
||||||
|
def ips_macos(self):
|
||||||
|
eps = {}
|
||||||
|
try:
|
||||||
|
txt, _ = chkcmd(["ifconfig"])
|
||||||
|
except:
|
||||||
|
return eps
|
||||||
|
|
||||||
|
rdev = re.compile(r"^([^ ]+):")
|
||||||
|
rip = re.compile(r"^\tinet ([0-9\.]+) ")
|
||||||
|
dev = None
|
||||||
|
for ln in txt.split("\n"):
|
||||||
|
m = rdev.match(ln)
|
||||||
|
if m:
|
||||||
|
dev = m.group(1)
|
||||||
|
|
||||||
|
m = rip.match(ln)
|
||||||
|
if m:
|
||||||
|
eps[m.group(1)] = dev
|
||||||
|
dev = None
|
||||||
|
|
||||||
|
return eps
|
||||||
|
|
||||||
|
def ips_windows_ipconfig(self):
|
||||||
|
eps = {}
|
||||||
|
try:
|
||||||
|
txt, _ = chkcmd(["ipconfig"])
|
||||||
|
except:
|
||||||
|
return eps
|
||||||
|
|
||||||
|
rdev = re.compile(r"(^[^ ].*):$")
|
||||||
|
rip = re.compile(r"^ +IPv?4? [^:]+: *([0-9\.]{7,15})$")
|
||||||
|
dev = None
|
||||||
|
for ln in txt.replace("\r", "").split("\n"):
|
||||||
|
m = rdev.match(ln)
|
||||||
|
if m:
|
||||||
|
dev = m.group(1).split(" adapter ", 1)[-1]
|
||||||
|
|
||||||
|
m = rip.match(ln)
|
||||||
|
if m and dev:
|
||||||
|
eps[m.group(1)] = dev
|
||||||
|
dev = None
|
||||||
|
|
||||||
|
return eps
|
||||||
|
|
||||||
|
def ips_windows_netsh(self):
|
||||||
|
eps = {}
|
||||||
|
try:
|
||||||
|
txt, _ = chkcmd("netsh interface ip show address".split())
|
||||||
|
except:
|
||||||
|
return eps
|
||||||
|
|
||||||
|
rdev = re.compile(r'.* "([^"]+)"$')
|
||||||
|
rip = re.compile(r".* IP\b.*: +([0-9\.]{7,15})$")
|
||||||
|
dev = None
|
||||||
|
for ln in txt.replace("\r", "").split("\n"):
|
||||||
|
m = rdev.match(ln)
|
||||||
|
if m:
|
||||||
|
dev = m.group(1)
|
||||||
|
|
||||||
|
m = rip.match(ln)
|
||||||
|
if m and dev:
|
||||||
|
eps[m.group(1)] = dev
|
||||||
|
dev = None
|
||||||
|
|
||||||
|
return eps
|
||||||
|
|
||||||
|
def detect_interfaces(self, listen_ips):
|
||||||
|
if MACOS:
|
||||||
|
eps = self.ips_macos()
|
||||||
|
elif ANYWIN:
|
||||||
|
eps = self.ips_windows_ipconfig() # sees more interfaces
|
||||||
|
eps.update(self.ips_windows_netsh()) # has better names
|
||||||
|
else:
|
||||||
|
eps = self.ips_linux()
|
||||||
|
|
||||||
|
if "0.0.0.0" not in listen_ips:
|
||||||
|
eps = {k: v for k, v in eps.items() if k in listen_ips}
|
||||||
|
|
||||||
default_route = None
|
default_route = None
|
||||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
for ip in [
|
for ip in [
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import os
|
|||||||
|
|
||||||
from .util import Cooldown
|
from .util import Cooldown
|
||||||
from .th_srv import thumb_path, THUMBABLE, FMT_FF
|
from .th_srv import thumb_path, THUMBABLE, FMT_FF
|
||||||
|
from .bos import bos
|
||||||
|
|
||||||
|
|
||||||
class ThumbCli(object):
|
class ThumbCli(object):
|
||||||
@@ -25,6 +26,9 @@ class ThumbCli(object):
|
|||||||
if is_vid and self.args.no_vthumb:
|
if is_vid and self.args.no_vthumb:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
if rem.startswith(".hist/th/") and rem.split(".")[-1] in ["webp", "jpg"]:
|
||||||
|
return os.path.join(ptop, rem)
|
||||||
|
|
||||||
if fmt == "j" and self.args.th_no_jpg:
|
if fmt == "j" and self.args.th_no_jpg:
|
||||||
fmt = "w"
|
fmt = "w"
|
||||||
|
|
||||||
@@ -36,7 +40,7 @@ class ThumbCli(object):
|
|||||||
tpath = thumb_path(histpath, rem, mtime, fmt)
|
tpath = thumb_path(histpath, rem, mtime, fmt)
|
||||||
ret = None
|
ret = None
|
||||||
try:
|
try:
|
||||||
st = os.stat(tpath)
|
st = bos.stat(tpath)
|
||||||
if st.st_size:
|
if st.st_size:
|
||||||
ret = tpath
|
ret = tpath
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import threading
|
|||||||
import subprocess as sp
|
import subprocess as sp
|
||||||
|
|
||||||
from .__init__ import PY2, unicode
|
from .__init__ import PY2, unicode
|
||||||
from .util import fsenc, runcmd, Queue, Cooldown, BytesIO, min_ex
|
from .util import fsenc, vsplit, runcmd, Queue, Cooldown, BytesIO, min_ex
|
||||||
|
from .bos import bos
|
||||||
from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, ffprobe
|
from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, ffprobe
|
||||||
|
|
||||||
|
|
||||||
@@ -20,7 +21,7 @@ HAVE_AVIF = False
|
|||||||
HAVE_WEBP = False
|
HAVE_WEBP = False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from PIL import Image, ImageOps
|
from PIL import Image, ImageOps, ExifTags
|
||||||
|
|
||||||
HAVE_PIL = True
|
HAVE_PIL = True
|
||||||
try:
|
try:
|
||||||
@@ -49,7 +50,7 @@ except:
|
|||||||
# https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html
|
# https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html
|
||||||
# ffmpeg -formats
|
# ffmpeg -formats
|
||||||
FMT_PIL = "bmp dib gif icns ico jpg jpeg jp2 jpx pcx png pbm pgm ppm pnm sgi tga tif tiff webp xbm dds xpm"
|
FMT_PIL = "bmp dib gif icns ico jpg jpeg jp2 jpx pcx png pbm pgm ppm pnm sgi tga tif tiff webp xbm dds xpm"
|
||||||
FMT_FF = "av1 asf avi flv m4v mkv mjpeg mjpg mpg mpeg mpg2 mpeg2 h264 avc h265 hevc mov 3gp mp4 ts mpegts nut ogv ogm rm vob webm wmv"
|
FMT_FF = "av1 asf avi flv m4v mkv mjpeg mjpg mpg mpeg mpg2 mpeg2 h264 avc mts h265 hevc mov 3gp mp4 ts mpegts nut ogv ogm rm vob webm wmv"
|
||||||
|
|
||||||
if HAVE_HEIF:
|
if HAVE_HEIF:
|
||||||
FMT_PIL += " heif heifs heic heics"
|
FMT_PIL += " heif heifs heic heics"
|
||||||
@@ -73,12 +74,7 @@ def thumb_path(histpath, rem, mtime, fmt):
|
|||||||
# base16 = 16 = 256
|
# base16 = 16 = 256
|
||||||
# b64-lc = 38 = 1444
|
# b64-lc = 38 = 1444
|
||||||
# base64 = 64 = 4096
|
# base64 = 64 = 4096
|
||||||
try:
|
rd, fn = vsplit(rem)
|
||||||
rd, fn = rem.rsplit("/", 1)
|
|
||||||
except:
|
|
||||||
rd = ""
|
|
||||||
fn = rem
|
|
||||||
|
|
||||||
if rd:
|
if rd:
|
||||||
h = hashlib.sha512(fsenc(rd)).digest()
|
h = hashlib.sha512(fsenc(rd)).digest()
|
||||||
b64 = base64.urlsafe_b64encode(h).decode("ascii")[:24]
|
b64 = base64.urlsafe_b64encode(h).decode("ascii")[:24]
|
||||||
@@ -109,7 +105,10 @@ class ThumbSrv(object):
|
|||||||
self.mutex = threading.Lock()
|
self.mutex = threading.Lock()
|
||||||
self.busy = {}
|
self.busy = {}
|
||||||
self.stopping = False
|
self.stopping = False
|
||||||
|
self.nthr = self.args.th_mt
|
||||||
|
if not self.nthr:
|
||||||
self.nthr = os.cpu_count() if hasattr(os, "cpu_count") else 4
|
self.nthr = os.cpu_count() if hasattr(os, "cpu_count") else 4
|
||||||
|
|
||||||
self.q = Queue(self.nthr * 4)
|
self.q = Queue(self.nthr * 4)
|
||||||
for n in range(self.nthr):
|
for n in range(self.nthr):
|
||||||
t = threading.Thread(
|
t = threading.Thread(
|
||||||
@@ -121,15 +120,16 @@ class ThumbSrv(object):
|
|||||||
if not self.args.no_vthumb and (not HAVE_FFMPEG or not HAVE_FFPROBE):
|
if not self.args.no_vthumb and (not HAVE_FFMPEG or not HAVE_FFPROBE):
|
||||||
missing = []
|
missing = []
|
||||||
if not HAVE_FFMPEG:
|
if not HAVE_FFMPEG:
|
||||||
missing.append("ffmpeg")
|
missing.append("FFmpeg")
|
||||||
|
|
||||||
if not HAVE_FFPROBE:
|
if not HAVE_FFPROBE:
|
||||||
missing.append("ffprobe")
|
missing.append("FFprobe")
|
||||||
|
|
||||||
msg = "cannot create video thumbnails because some of the required programs are not available: "
|
msg = "cannot create video thumbnails because some of the required programs are not available: "
|
||||||
msg += ", ".join(missing)
|
msg += ", ".join(missing)
|
||||||
self.log(msg, c=3)
|
self.log(msg, c=3)
|
||||||
|
|
||||||
|
if self.args.th_clean:
|
||||||
t = threading.Thread(target=self.cleaner, name="thumb-cleaner")
|
t = threading.Thread(target=self.cleaner, name="thumb-cleaner")
|
||||||
t.daemon = True
|
t.daemon = True
|
||||||
t.start()
|
t.start()
|
||||||
@@ -158,13 +158,10 @@ class ThumbSrv(object):
|
|||||||
self.log("wait {}".format(tpath))
|
self.log("wait {}".format(tpath))
|
||||||
except:
|
except:
|
||||||
thdir = os.path.dirname(tpath)
|
thdir = os.path.dirname(tpath)
|
||||||
try:
|
bos.makedirs(thdir)
|
||||||
os.makedirs(thdir)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
inf_path = os.path.join(thdir, "dir.txt")
|
inf_path = os.path.join(thdir, "dir.txt")
|
||||||
if not os.path.exists(inf_path):
|
if not bos.path.exists(inf_path):
|
||||||
with open(inf_path, "wb") as f:
|
with open(inf_path, "wb") as f:
|
||||||
f.write(fsenc(os.path.dirname(abspath)))
|
f.write(fsenc(os.path.dirname(abspath)))
|
||||||
|
|
||||||
@@ -184,7 +181,7 @@ class ThumbSrv(object):
|
|||||||
cond.wait(3)
|
cond.wait(3)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
st = os.stat(tpath)
|
st = bos.stat(tpath)
|
||||||
if st.st_size:
|
if st.st_size:
|
||||||
return tpath
|
return tpath
|
||||||
except:
|
except:
|
||||||
@@ -201,7 +198,7 @@ class ThumbSrv(object):
|
|||||||
abspath, tpath = task
|
abspath, tpath = task
|
||||||
ext = abspath.split(".")[-1].lower()
|
ext = abspath.split(".")[-1].lower()
|
||||||
fun = None
|
fun = None
|
||||||
if not os.path.exists(tpath):
|
if not bos.path.exists(tpath):
|
||||||
if ext in FMT_PIL:
|
if ext in FMT_PIL:
|
||||||
fun = self.conv_pil
|
fun = self.conv_pil
|
||||||
elif ext in FMT_FF:
|
elif ext in FMT_FF:
|
||||||
@@ -211,8 +208,8 @@ class ThumbSrv(object):
|
|||||||
try:
|
try:
|
||||||
fun(abspath, tpath)
|
fun(abspath, tpath)
|
||||||
except:
|
except:
|
||||||
msg = "{} failed on {}\n{}"
|
msg = "{} could not create thumbnail of {}\n{}"
|
||||||
self.log(msg.format(fun.__name__, abspath, min_ex()), 3)
|
self.log(msg.format(fun.__name__, abspath, min_ex()), "1;30")
|
||||||
with open(tpath, "wb") as _:
|
with open(tpath, "wb") as _:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -227,21 +224,38 @@ class ThumbSrv(object):
|
|||||||
with self.mutex:
|
with self.mutex:
|
||||||
self.nthr -= 1
|
self.nthr -= 1
|
||||||
|
|
||||||
def conv_pil(self, abspath, tpath):
|
def fancy_pillow(self, im):
|
||||||
with Image.open(fsenc(abspath)) as im:
|
# exif_transpose is expensive (loads full image + unconditional copy)
|
||||||
crop = not self.args.th_no_crop
|
r = max(*self.res) * 2
|
||||||
res2 = self.res
|
im.thumbnail((r, r), resample=Image.LANCZOS)
|
||||||
if crop:
|
|
||||||
res2 = (res2[0] * 2, res2[1] * 2)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
im.thumbnail(res2, resample=Image.LANCZOS)
|
k = next(k for k, v in ExifTags.TAGS.items() if v == "Orientation")
|
||||||
if crop:
|
exif = im.getexif()
|
||||||
|
rot = int(exif[k])
|
||||||
|
del exif[k]
|
||||||
|
except:
|
||||||
|
rot = 1
|
||||||
|
|
||||||
|
rots = {8: Image.ROTATE_90, 3: Image.ROTATE_180, 6: Image.ROTATE_270}
|
||||||
|
if rot in rots:
|
||||||
|
im = im.transpose(rots[rot])
|
||||||
|
|
||||||
|
if self.args.th_no_crop:
|
||||||
|
im.thumbnail(self.res, resample=Image.LANCZOS)
|
||||||
|
else:
|
||||||
iw, ih = im.size
|
iw, ih = im.size
|
||||||
dw, dh = self.res
|
dw, dh = self.res
|
||||||
res = (min(iw, dw), min(ih, dh))
|
res = (min(iw, dw), min(ih, dh))
|
||||||
im = ImageOps.fit(im, res, method=Image.LANCZOS)
|
im = ImageOps.fit(im, res, method=Image.LANCZOS)
|
||||||
except:
|
|
||||||
|
return im
|
||||||
|
|
||||||
|
def conv_pil(self, abspath, tpath):
|
||||||
|
with Image.open(fsenc(abspath)) as im:
|
||||||
|
try:
|
||||||
|
im = self.fancy_pillow(im)
|
||||||
|
except Exception as ex:
|
||||||
|
self.log("fancy_pillow {}".format(ex), "1;30")
|
||||||
im.thumbnail(self.res)
|
im.thumbnail(self.res)
|
||||||
|
|
||||||
fmts = ["RGB", "L"]
|
fmts = ["RGB", "L"]
|
||||||
@@ -256,13 +270,14 @@ class ThumbSrv(object):
|
|||||||
fmts += ["RGBA", "LA"]
|
fmts += ["RGBA", "LA"]
|
||||||
args["method"] = 6
|
args["method"] = 6
|
||||||
else:
|
else:
|
||||||
pass # default q = 75
|
# default q = 75
|
||||||
|
args["progressive"] = True
|
||||||
|
|
||||||
if im.mode not in fmts:
|
if im.mode not in fmts:
|
||||||
print("conv {}".format(im.mode))
|
# print("conv {}".format(im.mode))
|
||||||
im = im.convert("RGB")
|
im = im.convert("RGB")
|
||||||
|
|
||||||
im.save(tpath, quality=40, method=6)
|
im.save(tpath, **args)
|
||||||
|
|
||||||
def conv_ffmpeg(self, abspath, tpath):
|
def conv_ffmpeg(self, abspath, tpath):
|
||||||
ret, _ = ffprobe(abspath)
|
ret, _ = ffprobe(abspath)
|
||||||
@@ -292,8 +307,10 @@ class ThumbSrv(object):
|
|||||||
cmd += seek
|
cmd += seek
|
||||||
cmd += [
|
cmd += [
|
||||||
b"-i", fsenc(abspath),
|
b"-i", fsenc(abspath),
|
||||||
|
b"-map", b"0:v:0",
|
||||||
b"-vf", scale,
|
b"-vf", scale,
|
||||||
b"-vframes", b"1",
|
b"-frames:v", b"1",
|
||||||
|
b"-metadata:s:v:0", b"rotate=0",
|
||||||
]
|
]
|
||||||
# fmt: on
|
# fmt: on
|
||||||
|
|
||||||
@@ -311,11 +328,13 @@ class ThumbSrv(object):
|
|||||||
]
|
]
|
||||||
|
|
||||||
cmd += [fsenc(tpath)]
|
cmd += [fsenc(tpath)]
|
||||||
|
# self.log((b" ".join(cmd)).decode("utf-8"))
|
||||||
|
|
||||||
ret, sout, serr = runcmd(*cmd)
|
ret, sout, serr = runcmd(cmd)
|
||||||
if ret != 0:
|
if ret != 0:
|
||||||
msg = ["ff: {}".format(x) for x in serr.split("\n")]
|
m = "FFmpeg failed (probably a corrupt video file):\n"
|
||||||
self.log("FFmpeg failed:\n" + "\n".join(msg), c="1;30")
|
m += "\n".join(["ff: {}".format(x) for x in serr.split("\n")])
|
||||||
|
self.log(m, c="1;30")
|
||||||
raise sp.CalledProcessError(ret, (cmd[0], b"...", cmd[-1]))
|
raise sp.CalledProcessError(ret, (cmd[0], b"...", cmd[-1]))
|
||||||
|
|
||||||
def poke(self, tdir):
|
def poke(self, tdir):
|
||||||
@@ -327,7 +346,7 @@ class ThumbSrv(object):
|
|||||||
p1 = os.path.dirname(tdir)
|
p1 = os.path.dirname(tdir)
|
||||||
p2 = os.path.dirname(p1)
|
p2 = os.path.dirname(p1)
|
||||||
for dp in [tdir, p1, p2]:
|
for dp in [tdir, p1, p2]:
|
||||||
os.utime(fsenc(dp), (ts, ts))
|
bos.utime(dp, (ts, ts))
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -354,7 +373,7 @@ class ThumbSrv(object):
|
|||||||
prev_b64 = None
|
prev_b64 = None
|
||||||
prev_fp = None
|
prev_fp = None
|
||||||
try:
|
try:
|
||||||
ents = os.listdir(thumbpath)
|
ents = bos.listdir(thumbpath)
|
||||||
except:
|
except:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
@@ -365,7 +384,7 @@ class ThumbSrv(object):
|
|||||||
|
|
||||||
# "top" or b64 prefix/full (a folder)
|
# "top" or b64 prefix/full (a folder)
|
||||||
if len(f) <= 3 or len(f) == 24:
|
if len(f) <= 3 or len(f) == 24:
|
||||||
age = now - os.path.getmtime(fp)
|
age = now - bos.path.getmtime(fp)
|
||||||
if age > maxage:
|
if age > maxage:
|
||||||
with self.mutex:
|
with self.mutex:
|
||||||
safe = True
|
safe = True
|
||||||
@@ -397,7 +416,7 @@ class ThumbSrv(object):
|
|||||||
|
|
||||||
if b64 == prev_b64:
|
if b64 == prev_b64:
|
||||||
self.log("rm replaced [{}]".format(fp))
|
self.log("rm replaced [{}]".format(fp))
|
||||||
os.unlink(prev_fp)
|
bos.unlink(prev_fp)
|
||||||
|
|
||||||
prev_b64 = b64
|
prev_b64 = b64
|
||||||
prev_fp = fp
|
prev_fp = fp
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import time
|
|||||||
import threading
|
import threading
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
from .__init__ import unicode
|
||||||
from .util import s3dec, Pebkac, min_ex
|
from .util import s3dec, Pebkac, min_ex
|
||||||
|
from .bos import bos
|
||||||
from .up2k import up2k_wark_from_hashlist
|
from .up2k import up2k_wark_from_hashlist
|
||||||
|
|
||||||
|
|
||||||
@@ -66,7 +68,7 @@ class U2idx(object):
|
|||||||
|
|
||||||
histpath = self.asrv.vfs.histtab[ptop]
|
histpath = self.asrv.vfs.histtab[ptop]
|
||||||
db_path = os.path.join(histpath, "up2k.db")
|
db_path = os.path.join(histpath, "up2k.db")
|
||||||
if not os.path.exists(db_path):
|
if not bos.path.exists(db_path):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
cur = sqlite3.connect(db_path, 2).cursor()
|
cur = sqlite3.connect(db_path, 2).cursor()
|
||||||
@@ -86,10 +88,12 @@ class U2idx(object):
|
|||||||
is_date = False
|
is_date = False
|
||||||
kw_key = ["(", ")", "and ", "or ", "not "]
|
kw_key = ["(", ")", "and ", "or ", "not "]
|
||||||
kw_val = ["==", "=", "!=", ">", ">=", "<", "<=", "like "]
|
kw_val = ["==", "=", "!=", ">", ">=", "<", "<=", "like "]
|
||||||
ptn_mt = re.compile(r"^\.?[a-z]+$")
|
ptn_mt = re.compile(r"^\.?[a-z_-]+$")
|
||||||
mt_ctr = 0
|
mt_ctr = 0
|
||||||
mt_keycmp = "substr(up.w,1,16)"
|
mt_keycmp = "substr(up.w,1,16)"
|
||||||
mt_keycmp2 = None
|
mt_keycmp2 = None
|
||||||
|
ptn_lc = re.compile(r" (mt[0-9]+\.v) ([=<!>]+) \? $")
|
||||||
|
ptn_lcv = re.compile(r"[a-zA-Z]")
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
uq = uq.strip()
|
uq = uq.strip()
|
||||||
@@ -182,6 +186,21 @@ class U2idx(object):
|
|||||||
va.append(v)
|
va.append(v)
|
||||||
is_key = True
|
is_key = True
|
||||||
|
|
||||||
|
# lowercase tag searches
|
||||||
|
m = ptn_lc.search(q)
|
||||||
|
if not m or not ptn_lcv.search(unicode(v)):
|
||||||
|
continue
|
||||||
|
|
||||||
|
va.pop()
|
||||||
|
va.append(v.lower())
|
||||||
|
q = q[: m.start()]
|
||||||
|
|
||||||
|
field, oper = m.groups()
|
||||||
|
if oper in ["=", "=="]:
|
||||||
|
q += " {} like ? ".format(field)
|
||||||
|
else:
|
||||||
|
q += " lower({}) {} ? ".format(field, oper)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return self.run_query(vols, joins + "where " + q, va)
|
return self.run_query(vols, joins + "where " + q, va)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
@@ -225,7 +244,7 @@ class U2idx(object):
|
|||||||
sret = []
|
sret = []
|
||||||
c = cur.execute(q, v)
|
c = cur.execute(q, v)
|
||||||
for hit in c:
|
for hit in c:
|
||||||
w, ts, sz, rd, fn = hit
|
w, ts, sz, rd, fn, ip, at = hit
|
||||||
lim -= 1
|
lim -= 1
|
||||||
if lim <= 0:
|
if lim <= 0:
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -23,15 +23,20 @@ from .util import (
|
|||||||
ProgressPrinter,
|
ProgressPrinter,
|
||||||
fsdec,
|
fsdec,
|
||||||
fsenc,
|
fsenc,
|
||||||
|
absreal,
|
||||||
sanitize_fn,
|
sanitize_fn,
|
||||||
ren_open,
|
ren_open,
|
||||||
atomic_move,
|
atomic_move,
|
||||||
|
vsplit,
|
||||||
s3enc,
|
s3enc,
|
||||||
s3dec,
|
s3dec,
|
||||||
|
rmdirs,
|
||||||
statdir,
|
statdir,
|
||||||
s2hms,
|
s2hms,
|
||||||
min_ex,
|
min_ex,
|
||||||
)
|
)
|
||||||
|
from .bos import bos
|
||||||
|
from .authsrv import AuthSrv, LEELOO_DALLAS
|
||||||
from .mtag import MTag, MParser
|
from .mtag import MTag, MParser
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -40,20 +45,13 @@ try:
|
|||||||
except:
|
except:
|
||||||
HAVE_SQLITE3 = False
|
HAVE_SQLITE3 = False
|
||||||
|
|
||||||
DB_VER = 4
|
DB_VER = 5
|
||||||
|
|
||||||
|
|
||||||
class Up2k(object):
|
class Up2k(object):
|
||||||
"""
|
|
||||||
TODO:
|
|
||||||
* documentation
|
|
||||||
* registry persistence
|
|
||||||
* ~/.config flatfiles for active jobs
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, hub):
|
def __init__(self, hub):
|
||||||
self.hub = hub
|
self.hub = hub
|
||||||
self.asrv = hub.asrv
|
self.asrv = hub.asrv # type: AuthSrv
|
||||||
self.args = hub.args
|
self.args = hub.args
|
||||||
self.log_func = hub.log
|
self.log_func = hub.log
|
||||||
|
|
||||||
@@ -67,6 +65,7 @@ class Up2k(object):
|
|||||||
self.n_hashq = 0
|
self.n_hashq = 0
|
||||||
self.n_tagq = 0
|
self.n_tagq = 0
|
||||||
self.volstate = {}
|
self.volstate = {}
|
||||||
|
self.need_rescan = {}
|
||||||
self.registry = {}
|
self.registry = {}
|
||||||
self.entags = {}
|
self.entags = {}
|
||||||
self.flags = {}
|
self.flags = {}
|
||||||
@@ -101,17 +100,16 @@ class Up2k(object):
|
|||||||
|
|
||||||
if self.args.no_fastboot:
|
if self.args.no_fastboot:
|
||||||
self.deferred_init()
|
self.deferred_init()
|
||||||
else:
|
|
||||||
t = threading.Thread(
|
def init_vols(self):
|
||||||
target=self.deferred_init, name="up2k-deferred-init", args=(0.5,)
|
if self.args.no_fastboot:
|
||||||
)
|
return
|
||||||
|
|
||||||
|
t = threading.Thread(target=self.deferred_init, name="up2k-deferred-init")
|
||||||
t.daemon = True
|
t.daemon = True
|
||||||
t.start()
|
t.start()
|
||||||
|
|
||||||
def deferred_init(self, wait=0):
|
def deferred_init(self):
|
||||||
if wait:
|
|
||||||
time.sleep(wait)
|
|
||||||
|
|
||||||
all_vols = self.asrv.vfs.all_vols
|
all_vols = self.asrv.vfs.all_vols
|
||||||
have_e2d = self.init_indexes(all_vols)
|
have_e2d = self.init_indexes(all_vols)
|
||||||
|
|
||||||
@@ -124,6 +122,10 @@ class Up2k(object):
|
|||||||
thr.daemon = True
|
thr.daemon = True
|
||||||
thr.start()
|
thr.start()
|
||||||
|
|
||||||
|
thr = threading.Thread(target=self._sched_rescan, name="up2k-rescan")
|
||||||
|
thr.daemon = True
|
||||||
|
thr.start()
|
||||||
|
|
||||||
if self.mtag:
|
if self.mtag:
|
||||||
thr = threading.Thread(target=self._tagger, name="up2k-tagger")
|
thr = threading.Thread(target=self._tagger, name="up2k-tagger")
|
||||||
thr.daemon = True
|
thr.daemon = True
|
||||||
@@ -173,6 +175,73 @@ class Up2k(object):
|
|||||||
t.start()
|
t.start()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def _sched_rescan(self):
|
||||||
|
volage = {}
|
||||||
|
while True:
|
||||||
|
time.sleep(self.args.re_int)
|
||||||
|
now = time.time()
|
||||||
|
with self.mutex:
|
||||||
|
for vp, vol in sorted(self.asrv.vfs.all_vols.items()):
|
||||||
|
maxage = vol.flags.get("scan")
|
||||||
|
if not maxage:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if vp not in volage:
|
||||||
|
volage[vp] = now
|
||||||
|
|
||||||
|
if now - volage[vp] >= maxage:
|
||||||
|
self.need_rescan[vp] = 1
|
||||||
|
|
||||||
|
vols = list(sorted(self.need_rescan.keys()))
|
||||||
|
self.need_rescan = {}
|
||||||
|
|
||||||
|
if vols:
|
||||||
|
err = self.rescan(self.asrv.vfs.all_vols, vols)
|
||||||
|
if err:
|
||||||
|
for v in vols:
|
||||||
|
self.need_rescan[v] = True
|
||||||
|
|
||||||
|
continue
|
||||||
|
|
||||||
|
for v in vols:
|
||||||
|
volage[v] = now
|
||||||
|
|
||||||
|
if self.args.no_lifetime:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for vp, vol in sorted(self.asrv.vfs.all_vols.items()):
|
||||||
|
lifetime = vol.flags.get("lifetime")
|
||||||
|
if not lifetime:
|
||||||
|
continue
|
||||||
|
|
||||||
|
cur = self.cur.get(vol.realpath)
|
||||||
|
if not cur:
|
||||||
|
continue
|
||||||
|
|
||||||
|
nrm = 0
|
||||||
|
deadline = time.time() - int(lifetime)
|
||||||
|
q = "select rd, fn from up where at > 0 and at < ? limit 100"
|
||||||
|
while True:
|
||||||
|
with self.mutex:
|
||||||
|
hits = cur.execute(q, (deadline,)).fetchall()
|
||||||
|
|
||||||
|
if not hits:
|
||||||
|
break
|
||||||
|
|
||||||
|
for rd, fn in hits:
|
||||||
|
if rd.startswith("//") or fn.startswith("//"):
|
||||||
|
rd, fn = s3dec(rd, fn)
|
||||||
|
|
||||||
|
fvp = "{}/{}".format(rd, fn).strip("/")
|
||||||
|
if vp:
|
||||||
|
fvp = "{}/{}".format(vp, fvp)
|
||||||
|
|
||||||
|
self._handle_rm(LEELOO_DALLAS, None, fvp)
|
||||||
|
nrm += 1
|
||||||
|
|
||||||
|
if nrm:
|
||||||
|
self.log("{} files graduated in {}".format(nrm, vp))
|
||||||
|
|
||||||
def _vis_job_progress(self, job):
|
def _vis_job_progress(self, job):
|
||||||
perc = 100 - (len(job["need"]) * 100.0 / len(job["hash"]))
|
perc = 100 - (len(job["need"]) * 100.0 / len(job["hash"]))
|
||||||
path = os.path.join(job["ptop"], job["prel"], job["name"])
|
path = os.path.join(job["ptop"], job["prel"], job["name"])
|
||||||
@@ -195,7 +264,7 @@ class Up2k(object):
|
|||||||
|
|
||||||
return True, ret
|
return True, ret
|
||||||
|
|
||||||
def init_indexes(self, all_vols, scan_vols=[]):
|
def init_indexes(self, all_vols, scan_vols=None):
|
||||||
self.pp = ProgressPrinter()
|
self.pp = ProgressPrinter()
|
||||||
vols = all_vols.values()
|
vols = all_vols.values()
|
||||||
t0 = time.time()
|
t0 = time.time()
|
||||||
@@ -218,7 +287,7 @@ class Up2k(object):
|
|||||||
# only need to protect register_vpath but all in one go feels right
|
# only need to protect register_vpath but all in one go feels right
|
||||||
for vol in vols:
|
for vol in vols:
|
||||||
try:
|
try:
|
||||||
os.listdir(vol.realpath)
|
bos.listdir(vol.realpath)
|
||||||
except:
|
except:
|
||||||
self.volstate[vol.vpath] = "OFFLINE (cannot access folder)"
|
self.volstate[vol.vpath] = "OFFLINE (cannot access folder)"
|
||||||
self.log("cannot access " + vol.realpath, c=1)
|
self.log("cannot access " + vol.realpath, c=1)
|
||||||
@@ -304,7 +373,7 @@ class Up2k(object):
|
|||||||
self.log(msg.format(len(vols), time.time() - t0))
|
self.log(msg.format(len(vols), time.time() - t0))
|
||||||
|
|
||||||
if needed_mutagen:
|
if needed_mutagen:
|
||||||
msg = "could not read tags because no backends are available (mutagen or ffprobe)"
|
msg = "could not read tags because no backends are available (Mutagen or FFprobe)"
|
||||||
self.log(msg, c=1)
|
self.log(msg, c=1)
|
||||||
|
|
||||||
thr = None
|
thr = None
|
||||||
@@ -356,14 +425,14 @@ class Up2k(object):
|
|||||||
|
|
||||||
reg = {}
|
reg = {}
|
||||||
path = os.path.join(histpath, "up2k.snap")
|
path = os.path.join(histpath, "up2k.snap")
|
||||||
if "e2d" in flags and os.path.exists(path):
|
if "e2d" in flags and bos.path.exists(path):
|
||||||
with gzip.GzipFile(path, "rb") as f:
|
with gzip.GzipFile(path, "rb") as f:
|
||||||
j = f.read().decode("utf-8")
|
j = f.read().decode("utf-8")
|
||||||
|
|
||||||
reg2 = json.loads(j)
|
reg2 = json.loads(j)
|
||||||
for k, job in reg2.items():
|
for k, job in reg2.items():
|
||||||
path = os.path.join(job["ptop"], job["prel"], job["name"])
|
path = os.path.join(job["ptop"], job["prel"], job["name"])
|
||||||
if os.path.exists(fsenc(path)):
|
if bos.path.exists(path):
|
||||||
reg[k] = job
|
reg[k] = job
|
||||||
job["poke"] = time.time()
|
job["poke"] = time.time()
|
||||||
else:
|
else:
|
||||||
@@ -378,10 +447,7 @@ class Up2k(object):
|
|||||||
if not HAVE_SQLITE3 or "e2d" not in flags or "d2d" in flags:
|
if not HAVE_SQLITE3 or "e2d" not in flags or "d2d" in flags:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
bos.makedirs(histpath)
|
||||||
os.makedirs(histpath)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cur = self._open_db(db_path)
|
cur = self._open_db(db_path)
|
||||||
@@ -420,14 +486,7 @@ class Up2k(object):
|
|||||||
return True, n_add or n_rm or do_vac
|
return True, n_add or n_rm or do_vac
|
||||||
|
|
||||||
def _build_dir(self, dbw, top, excl, cdir, nohash, seen):
|
def _build_dir(self, dbw, top, excl, cdir, nohash, seen):
|
||||||
rcdir = cdir
|
rcdir = absreal(cdir) # a bit expensive but worth
|
||||||
if not ANYWIN:
|
|
||||||
try:
|
|
||||||
# a bit expensive but worth
|
|
||||||
rcdir = os.path.realpath(cdir)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if rcdir in seen:
|
if rcdir in seen:
|
||||||
m = "bailing from symlink loop,\n prev: {}\n curr: {}\n from: {}"
|
m = "bailing from symlink loop,\n prev: {}\n curr: {}\n from: {}"
|
||||||
self.log(m.format(seen[-1], rcdir, cdir), 3)
|
self.log(m.format(seen[-1], rcdir, cdir), 3)
|
||||||
@@ -498,7 +557,7 @@ class Up2k(object):
|
|||||||
|
|
||||||
wark = up2k_wark_from_hashlist(self.salt, sz, hashes)
|
wark = up2k_wark_from_hashlist(self.salt, sz, hashes)
|
||||||
|
|
||||||
self.db_add(dbw[0], wark, rd, fn, lmod, sz)
|
self.db_add(dbw[0], wark, rd, fn, lmod, sz, "", 0)
|
||||||
dbw[1] += 1
|
dbw[1] += 1
|
||||||
ret += 1
|
ret += 1
|
||||||
td = time.time() - dbw[2]
|
td = time.time() - dbw[2]
|
||||||
@@ -513,8 +572,8 @@ class Up2k(object):
|
|||||||
rm = []
|
rm = []
|
||||||
nchecked = 0
|
nchecked = 0
|
||||||
nfiles = next(cur.execute("select count(w) from up"))[0]
|
nfiles = next(cur.execute("select count(w) from up"))[0]
|
||||||
c = cur.execute("select * from up")
|
c = cur.execute("select rd, fn from up")
|
||||||
for dwark, dts, dsz, drd, dfn in c:
|
for drd, dfn in c:
|
||||||
nchecked += 1
|
nchecked += 1
|
||||||
if drd.startswith("//") or dfn.startswith("//"):
|
if drd.startswith("//") or dfn.startswith("//"):
|
||||||
drd, dfn = s3dec(drd, dfn)
|
drd, dfn = s3dec(drd, dfn)
|
||||||
@@ -523,7 +582,7 @@ class Up2k(object):
|
|||||||
# almost zero overhead dw
|
# almost zero overhead dw
|
||||||
self.pp.msg = "b{} {}".format(nfiles - nchecked, abspath)
|
self.pp.msg = "b{} {}".format(nfiles - nchecked, abspath)
|
||||||
try:
|
try:
|
||||||
if not os.path.exists(fsenc(abspath)):
|
if not bos.path.exists(abspath):
|
||||||
rm.append([drd, dfn])
|
rm.append([drd, dfn])
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
self.log("stat-rm: {} @ [{}]".format(repr(ex), abspath))
|
self.log("stat-rm: {} @ [{}]".format(repr(ex), abspath))
|
||||||
@@ -596,7 +655,7 @@ class Up2k(object):
|
|||||||
c2 = conn.cursor()
|
c2 = conn.cursor()
|
||||||
c3 = conn.cursor()
|
c3 = conn.cursor()
|
||||||
n_left = cur.execute("select count(w) from up").fetchone()[0]
|
n_left = cur.execute("select count(w) from up").fetchone()[0]
|
||||||
for w, rd, fn in cur.execute("select w, rd, fn from up"):
|
for w, rd, fn in cur.execute("select w, rd, fn from up order by rd, fn"):
|
||||||
n_left -= 1
|
n_left -= 1
|
||||||
q = "select w from mt where w = ?"
|
q = "select w from mt where w = ?"
|
||||||
if c2.execute(q, (w[:16],)).fetchone():
|
if c2.execute(q, (w[:16],)).fetchone():
|
||||||
@@ -911,12 +970,21 @@ class Up2k(object):
|
|||||||
# x.set_trace_callback(trace)
|
# x.set_trace_callback(trace)
|
||||||
|
|
||||||
def _open_db(self, db_path):
|
def _open_db(self, db_path):
|
||||||
existed = os.path.exists(db_path)
|
existed = bos.path.exists(db_path)
|
||||||
cur = self._orz(db_path)
|
cur = self._orz(db_path)
|
||||||
ver = self._read_ver(cur)
|
ver = self._read_ver(cur)
|
||||||
if not existed and ver is None:
|
if not existed and ver is None:
|
||||||
return self._create_db(db_path, cur)
|
return self._create_db(db_path, cur)
|
||||||
|
|
||||||
|
if ver == 4:
|
||||||
|
try:
|
||||||
|
m = "creating backup before upgrade: "
|
||||||
|
cur = self._backup_db(db_path, cur, ver, m)
|
||||||
|
self._upgrade_v4(cur)
|
||||||
|
ver = 5
|
||||||
|
except:
|
||||||
|
self.log("WARN: failed to upgrade from v4", 3)
|
||||||
|
|
||||||
if ver == DB_VER:
|
if ver == DB_VER:
|
||||||
try:
|
try:
|
||||||
nfiles = next(cur.execute("select count(w) from up"))[0]
|
nfiles = next(cur.execute("select count(w) from up"))[0]
|
||||||
@@ -929,19 +997,38 @@ class Up2k(object):
|
|||||||
m = "database is version {}, this copyparty only supports versions <= {}"
|
m = "database is version {}, this copyparty only supports versions <= {}"
|
||||||
raise Exception(m.format(ver, DB_VER))
|
raise Exception(m.format(ver, DB_VER))
|
||||||
|
|
||||||
bak = "{}.bak.{:x}.v{}".format(db_path, int(time.time()), ver)
|
|
||||||
db = cur.connection
|
|
||||||
cur.close()
|
|
||||||
db.close()
|
|
||||||
msg = "creating new DB (old is bad); backup: {}"
|
msg = "creating new DB (old is bad); backup: {}"
|
||||||
if ver:
|
if ver:
|
||||||
msg = "creating new DB (too old to upgrade); backup: {}"
|
msg = "creating new DB (too old to upgrade); backup: {}"
|
||||||
|
|
||||||
self.log(msg.format(bak))
|
cur = self._backup_db(db_path, cur, ver, msg)
|
||||||
os.rename(fsenc(db_path), fsenc(bak))
|
db = cur.connection
|
||||||
|
cur.close()
|
||||||
|
db.close()
|
||||||
|
bos.unlink(db_path)
|
||||||
return self._create_db(db_path, None)
|
return self._create_db(db_path, None)
|
||||||
|
|
||||||
|
def _backup_db(self, db_path, cur, ver, msg):
|
||||||
|
bak = "{}.bak.{:x}.v{}".format(db_path, int(time.time()), ver)
|
||||||
|
self.log(msg + bak)
|
||||||
|
try:
|
||||||
|
c2 = sqlite3.connect(bak)
|
||||||
|
with c2:
|
||||||
|
cur.connection.backup(c2)
|
||||||
|
return cur
|
||||||
|
except:
|
||||||
|
m = "native sqlite3 backup failed; using fallback method:\n"
|
||||||
|
self.log(m + min_ex())
|
||||||
|
finally:
|
||||||
|
c2.close()
|
||||||
|
|
||||||
|
db = cur.connection
|
||||||
|
cur.close()
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
shutil.copy2(fsenc(db_path), fsenc(bak))
|
||||||
|
return self._orz(db_path)
|
||||||
|
|
||||||
def _read_ver(self, cur):
|
def _read_ver(self, cur):
|
||||||
for tab in ["ki", "kv"]:
|
for tab in ["ki", "kv"]:
|
||||||
try:
|
try:
|
||||||
@@ -968,9 +1055,10 @@ class Up2k(object):
|
|||||||
idx = r"create index up_w on up(w)"
|
idx = r"create index up_w on up(w)"
|
||||||
|
|
||||||
for cmd in [
|
for cmd in [
|
||||||
r"create table up (w text, mt int, sz int, rd text, fn text)",
|
r"create table up (w text, mt int, sz int, rd text, fn text, ip text, at int)",
|
||||||
r"create index up_rd on up(rd)",
|
r"create index up_rd on up(rd)",
|
||||||
r"create index up_fn on up(fn)",
|
r"create index up_fn on up(fn)",
|
||||||
|
r"create index up_ip on up(ip)",
|
||||||
idx,
|
idx,
|
||||||
r"create table mt (w text, k text, v int)",
|
r"create table mt (w text, k text, v int)",
|
||||||
r"create index mt_w on mt(w)",
|
r"create index mt_w on mt(w)",
|
||||||
@@ -985,13 +1073,24 @@ class Up2k(object):
|
|||||||
self.log("created DB at {}".format(db_path))
|
self.log("created DB at {}".format(db_path))
|
||||||
return cur
|
return cur
|
||||||
|
|
||||||
|
def _upgrade_v4(self, cur):
|
||||||
|
for cmd in [
|
||||||
|
r"alter table up add column ip text",
|
||||||
|
r"alter table up add column at int",
|
||||||
|
r"create index up_ip on up(ip)",
|
||||||
|
r"update kv set v=5 where k='sver'",
|
||||||
|
]:
|
||||||
|
cur.execute(cmd)
|
||||||
|
|
||||||
|
cur.connection.commit()
|
||||||
|
|
||||||
def handle_json(self, cj):
|
def handle_json(self, cj):
|
||||||
with self.mutex:
|
with self.mutex:
|
||||||
if not self.register_vpath(cj["ptop"], cj["vcfg"]):
|
if not self.register_vpath(cj["ptop"], cj["vcfg"]):
|
||||||
if cj["ptop"] not in self.registry:
|
if cj["ptop"] not in self.registry:
|
||||||
raise Pebkac(410, "location unavailable")
|
raise Pebkac(410, "location unavailable")
|
||||||
|
|
||||||
cj["name"] = sanitize_fn(cj["name"], bad=[".prologue.html", ".epilogue.html"])
|
cj["name"] = sanitize_fn(cj["name"], "", [".prologue.html", ".epilogue.html"])
|
||||||
cj["poke"] = time.time()
|
cj["poke"] = time.time()
|
||||||
wark = self._get_wark(cj)
|
wark = self._get_wark(cj)
|
||||||
now = time.time()
|
now = time.time()
|
||||||
@@ -1008,13 +1107,13 @@ class Up2k(object):
|
|||||||
argv = (wark[:16], wark)
|
argv = (wark[:16], wark)
|
||||||
|
|
||||||
cur = cur.execute(q, argv)
|
cur = cur.execute(q, argv)
|
||||||
for _, dtime, dsize, dp_dir, dp_fn in cur:
|
for _, dtime, dsize, dp_dir, dp_fn, ip, at in cur:
|
||||||
if dp_dir.startswith("//") or dp_fn.startswith("//"):
|
if dp_dir.startswith("//") or dp_fn.startswith("//"):
|
||||||
dp_dir, dp_fn = s3dec(dp_dir, dp_fn)
|
dp_dir, dp_fn = s3dec(dp_dir, dp_fn)
|
||||||
|
|
||||||
dp_abs = "/".join([cj["ptop"], dp_dir, dp_fn])
|
dp_abs = "/".join([cj["ptop"], dp_dir, dp_fn])
|
||||||
# relying on path.exists to return false on broken symlinks
|
# relying on path.exists to return false on broken symlinks
|
||||||
if os.path.exists(fsenc(dp_abs)):
|
if bos.path.exists(dp_abs):
|
||||||
job = {
|
job = {
|
||||||
"name": dp_fn,
|
"name": dp_fn,
|
||||||
"prel": dp_dir,
|
"prel": dp_dir,
|
||||||
@@ -1022,6 +1121,8 @@ class Up2k(object):
|
|||||||
"ptop": cj["ptop"],
|
"ptop": cj["ptop"],
|
||||||
"size": dsize,
|
"size": dsize,
|
||||||
"lmod": dtime,
|
"lmod": dtime,
|
||||||
|
"addr": ip,
|
||||||
|
"at": at,
|
||||||
"hash": [],
|
"hash": [],
|
||||||
"need": [],
|
"need": [],
|
||||||
}
|
}
|
||||||
@@ -1038,7 +1139,7 @@ class Up2k(object):
|
|||||||
for fn in names:
|
for fn in names:
|
||||||
path = os.path.join(job["ptop"], job["prel"], fn)
|
path = os.path.join(job["ptop"], job["prel"], fn)
|
||||||
try:
|
try:
|
||||||
if os.path.getsize(fsenc(path)) > 0:
|
if bos.path.getsize(path) > 0:
|
||||||
# upload completed or both present
|
# upload completed or both present
|
||||||
break
|
break
|
||||||
except:
|
except:
|
||||||
@@ -1072,10 +1173,26 @@ class Up2k(object):
|
|||||||
job["name"] = self._untaken(pdir, cj["name"], now, cj["addr"])
|
job["name"] = self._untaken(pdir, cj["name"], now, cj["addr"])
|
||||||
dst = os.path.join(job["ptop"], job["prel"], job["name"])
|
dst = os.path.join(job["ptop"], job["prel"], job["name"])
|
||||||
if not self.args.nw:
|
if not self.args.nw:
|
||||||
os.unlink(fsenc(dst)) # TODO ed pls
|
bos.unlink(dst) # TODO ed pls
|
||||||
self._symlink(src, dst)
|
self._symlink(src, dst)
|
||||||
|
|
||||||
|
if cur:
|
||||||
|
a = [cj[x] for x in "prel name lmod size addr".split()]
|
||||||
|
a += [cj.get("at") or time.time()]
|
||||||
|
self.db_add(cur, wark, *a)
|
||||||
|
cur.connection.commit()
|
||||||
|
|
||||||
if not job:
|
if not job:
|
||||||
|
vfs = self.asrv.vfs.all_vols[cj["vtop"]]
|
||||||
|
if vfs.lim:
|
||||||
|
ap1 = os.path.join(cj["ptop"], cj["prel"])
|
||||||
|
ap2, cj["prel"] = vfs.lim.all(
|
||||||
|
cj["addr"], cj["prel"], cj["size"], ap1
|
||||||
|
)
|
||||||
|
bos.makedirs(ap2)
|
||||||
|
vfs.lim.nup(cj["addr"])
|
||||||
|
vfs.lim.bup(cj["addr"], cj["size"])
|
||||||
|
|
||||||
job = {
|
job = {
|
||||||
"wark": wark,
|
"wark": wark,
|
||||||
"t0": now,
|
"t0": now,
|
||||||
@@ -1106,8 +1223,12 @@ class Up2k(object):
|
|||||||
|
|
||||||
self._new_upload(job)
|
self._new_upload(job)
|
||||||
|
|
||||||
|
purl = "{}/{}".format(job["vtop"], job["prel"]).strip("/")
|
||||||
|
purl = "/{}/".format(purl) if purl else "/"
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"name": job["name"],
|
"name": job["name"],
|
||||||
|
"purl": purl,
|
||||||
"size": job["size"],
|
"size": job["size"],
|
||||||
"lmod": job["lmod"],
|
"lmod": job["lmod"],
|
||||||
"hash": job["need"],
|
"hash": job["need"],
|
||||||
@@ -1124,17 +1245,18 @@ class Up2k(object):
|
|||||||
with ren_open(fname, "wb", fdir=fdir, suffix=suffix) as f:
|
with ren_open(fname, "wb", fdir=fdir, suffix=suffix) as f:
|
||||||
return f["orz"][1]
|
return f["orz"][1]
|
||||||
|
|
||||||
def _symlink(self, src, dst):
|
def _symlink(self, src, dst, verbose=True):
|
||||||
# TODO store this in linktab so we never delete src if there are links to it
|
if verbose:
|
||||||
self.log("linking dupe:\n {0}\n {1}".format(src, dst))
|
self.log("linking dupe:\n {0}\n {1}".format(src, dst))
|
||||||
|
|
||||||
if self.args.nw:
|
if self.args.nw:
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
lsrc = src
|
lsrc = src
|
||||||
ldst = dst
|
ldst = dst
|
||||||
fs1 = os.stat(fsenc(os.path.split(src)[0])).st_dev
|
fs1 = bos.stat(os.path.dirname(src)).st_dev
|
||||||
fs2 = os.stat(fsenc(os.path.split(dst)[0])).st_dev
|
fs2 = bos.stat(os.path.dirname(dst)).st_dev
|
||||||
if fs1 == 0:
|
if fs1 == 0:
|
||||||
# py2 on winxp or other unsupported combination
|
# py2 on winxp or other unsupported combination
|
||||||
raise OSError()
|
raise OSError()
|
||||||
@@ -1157,7 +1279,7 @@ class Up2k(object):
|
|||||||
hops = len(ndst[nc:]) - 1
|
hops = len(ndst[nc:]) - 1
|
||||||
lsrc = "../" * hops + "/".join(lsrc)
|
lsrc = "../" * hops + "/".join(lsrc)
|
||||||
os.symlink(fsenc(lsrc), fsenc(ldst))
|
os.symlink(fsenc(lsrc), fsenc(ldst))
|
||||||
except (AttributeError, OSError) as ex:
|
except Exception as ex:
|
||||||
self.log("cannot symlink; creating copy: " + repr(ex))
|
self.log("cannot symlink; creating copy: " + repr(ex))
|
||||||
shutil.copy2(fsenc(src), fsenc(dst))
|
shutil.copy2(fsenc(src), fsenc(dst))
|
||||||
|
|
||||||
@@ -1217,27 +1339,21 @@ class Up2k(object):
|
|||||||
a = [dst, job["size"], (int(time.time()), int(job["lmod"]))]
|
a = [dst, job["size"], (int(time.time()), int(job["lmod"]))]
|
||||||
self.lastmod_q.put(a)
|
self.lastmod_q.put(a)
|
||||||
|
|
||||||
# legit api sware 2 me mum
|
a = [job[x] for x in "ptop wark prel name lmod size addr".split()]
|
||||||
if self.idx_wark(
|
a += [job.get("at") or time.time()]
|
||||||
job["ptop"],
|
if self.idx_wark(*a):
|
||||||
job["wark"],
|
|
||||||
job["prel"],
|
|
||||||
job["name"],
|
|
||||||
job["lmod"],
|
|
||||||
job["size"],
|
|
||||||
):
|
|
||||||
del self.registry[ptop][wark]
|
del self.registry[ptop][wark]
|
||||||
# in-memory registry is reserved for unfinished uploads
|
# in-memory registry is reserved for unfinished uploads
|
||||||
|
|
||||||
return ret, dst
|
return ret, dst
|
||||||
|
|
||||||
def idx_wark(self, ptop, wark, rd, fn, lmod, sz):
|
def idx_wark(self, ptop, wark, rd, fn, lmod, sz, ip, at):
|
||||||
cur = self.cur.get(ptop)
|
cur = self.cur.get(ptop)
|
||||||
if not cur:
|
if not cur:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self.db_rm(cur, rd, fn)
|
self.db_rm(cur, rd, fn)
|
||||||
self.db_add(cur, wark, rd, fn, int(lmod), sz)
|
self.db_add(cur, wark, rd, fn, lmod, sz, ip, at)
|
||||||
cur.connection.commit()
|
cur.connection.commit()
|
||||||
|
|
||||||
if "e2t" in self.flags[ptop]:
|
if "e2t" in self.flags[ptop]:
|
||||||
@@ -1253,16 +1369,326 @@ class Up2k(object):
|
|||||||
except:
|
except:
|
||||||
db.execute(sql, s3enc(self.mem_cur, rd, fn))
|
db.execute(sql, s3enc(self.mem_cur, rd, fn))
|
||||||
|
|
||||||
def db_add(self, db, wark, rd, fn, ts, sz):
|
def db_add(self, db, wark, rd, fn, ts, sz, ip, at):
|
||||||
sql = "insert into up values (?,?,?,?,?)"
|
sql = "insert into up values (?,?,?,?,?,?,?)"
|
||||||
v = (wark, int(ts), sz, rd, fn)
|
v = (wark, int(ts), sz, rd, fn, ip or "", int(at or 0))
|
||||||
try:
|
try:
|
||||||
db.execute(sql, v)
|
db.execute(sql, v)
|
||||||
except:
|
except:
|
||||||
rd, fn = s3enc(self.mem_cur, rd, fn)
|
rd, fn = s3enc(self.mem_cur, rd, fn)
|
||||||
v = (wark, ts, sz, rd, fn)
|
v = (wark, int(ts), sz, rd, fn, ip or "", int(at or 0))
|
||||||
db.execute(sql, v)
|
db.execute(sql, v)
|
||||||
|
|
||||||
|
def handle_rm(self, uname, ip, vpaths):
|
||||||
|
n_files = 0
|
||||||
|
ok = {}
|
||||||
|
ng = {}
|
||||||
|
for vp in vpaths:
|
||||||
|
a, b, c = self._handle_rm(uname, ip, vp)
|
||||||
|
n_files += a
|
||||||
|
for k in b:
|
||||||
|
ok[k] = 1
|
||||||
|
for k in c:
|
||||||
|
ng[k] = 1
|
||||||
|
|
||||||
|
ng = {k: 1 for k in ng if k not in ok}
|
||||||
|
ok = len(ok)
|
||||||
|
ng = len(ng)
|
||||||
|
|
||||||
|
return "deleted {} files (and {}/{} folders)".format(n_files, ok, ok + ng)
|
||||||
|
|
||||||
|
def _handle_rm(self, uname, ip, vpath):
|
||||||
|
try:
|
||||||
|
permsets = [[True, False, False, True]]
|
||||||
|
vn, rem = self.asrv.vfs.get(vpath, uname, *permsets[0])
|
||||||
|
unpost = False
|
||||||
|
except:
|
||||||
|
# unpost with missing permissions? try read+write and verify with db
|
||||||
|
if not self.args.unpost:
|
||||||
|
raise Pebkac(400, "the unpost feature is disabled in server config")
|
||||||
|
|
||||||
|
unpost = True
|
||||||
|
permsets = [[True, True]]
|
||||||
|
vn, rem = self.asrv.vfs.get(vpath, uname, *permsets[0])
|
||||||
|
_, _, _, _, dip, dat = self._find_from_vpath(vn.realpath, rem)
|
||||||
|
|
||||||
|
m = "you cannot delete this: "
|
||||||
|
if not dip:
|
||||||
|
m += "file not found"
|
||||||
|
elif dip != ip:
|
||||||
|
m += "not uploaded by (You)"
|
||||||
|
elif dat < time.time() - self.args.unpost:
|
||||||
|
m += "uploaded too long ago"
|
||||||
|
else:
|
||||||
|
m = None
|
||||||
|
|
||||||
|
if m:
|
||||||
|
raise Pebkac(400, m)
|
||||||
|
|
||||||
|
ptop = vn.realpath
|
||||||
|
atop = vn.canonical(rem, False)
|
||||||
|
adir, fn = os.path.split(atop)
|
||||||
|
try:
|
||||||
|
st = bos.lstat(atop)
|
||||||
|
except:
|
||||||
|
raise Pebkac(400, "file not found on disk (already deleted?)")
|
||||||
|
|
||||||
|
scandir = not self.args.no_scandir
|
||||||
|
if stat.S_ISLNK(st.st_mode) or stat.S_ISREG(st.st_mode):
|
||||||
|
dbv, vrem = self.asrv.vfs.get(vpath, uname, *permsets[0])
|
||||||
|
dbv, vrem = dbv.get_dbv(vrem)
|
||||||
|
voldir = vsplit(vrem)[0]
|
||||||
|
vpath_dir = vsplit(vpath)[0]
|
||||||
|
g = [[dbv, voldir, vpath_dir, adir, [[fn, 0]], [], []]]
|
||||||
|
else:
|
||||||
|
g = vn.walk("", rem, [], uname, permsets, True, scandir, True)
|
||||||
|
if unpost:
|
||||||
|
raise Pebkac(400, "cannot unpost folders")
|
||||||
|
|
||||||
|
n_files = 0
|
||||||
|
for dbv, vrem, _, adir, files, rd, vd in g:
|
||||||
|
for fn in [x[0] for x in files]:
|
||||||
|
n_files += 1
|
||||||
|
abspath = os.path.join(adir, fn)
|
||||||
|
volpath = "{}/{}".format(vrem, fn).strip("/")
|
||||||
|
vpath = "{}/{}".format(dbv.vpath, volpath).strip("/")
|
||||||
|
self.log("rm {}\n {}".format(vpath, abspath))
|
||||||
|
_ = dbv.get(volpath, uname, *permsets[0])
|
||||||
|
with self.mutex:
|
||||||
|
cur = None
|
||||||
|
try:
|
||||||
|
ptop = dbv.realpath
|
||||||
|
cur, wark, _, _, _, _ = self._find_from_vpath(ptop, volpath)
|
||||||
|
self._forget_file(ptop, volpath, cur, wark, True)
|
||||||
|
finally:
|
||||||
|
if cur:
|
||||||
|
cur.connection.commit()
|
||||||
|
|
||||||
|
bos.unlink(abspath)
|
||||||
|
|
||||||
|
rm = rmdirs(self.log_func, scandir, True, atop)
|
||||||
|
return n_files, rm[0], rm[1]
|
||||||
|
|
||||||
|
def handle_mv(self, uname, svp, dvp):
|
||||||
|
svn, srem = self.asrv.vfs.get(svp, uname, True, False, True)
|
||||||
|
svn, srem = svn.get_dbv(srem)
|
||||||
|
sabs = svn.canonical(srem, False)
|
||||||
|
|
||||||
|
if not srem:
|
||||||
|
raise Pebkac(400, "mv: cannot move a mountpoint")
|
||||||
|
|
||||||
|
st = bos.lstat(sabs)
|
||||||
|
if stat.S_ISREG(st.st_mode) or stat.S_ISLNK(st.st_mode):
|
||||||
|
with self.mutex:
|
||||||
|
return self._mv_file(uname, svp, dvp)
|
||||||
|
|
||||||
|
jail = svn.get_dbv(srem)[0]
|
||||||
|
permsets = [[True, False, True]]
|
||||||
|
scandir = not self.args.no_scandir
|
||||||
|
|
||||||
|
# following symlinks is too scary
|
||||||
|
g = svn.walk("", srem, [], uname, permsets, True, scandir, True)
|
||||||
|
for dbv, vrem, _, atop, files, rd, vd in g:
|
||||||
|
if dbv != jail:
|
||||||
|
# fail early (prevent partial moves)
|
||||||
|
raise Pebkac(400, "mv: source folder contains other volumes")
|
||||||
|
|
||||||
|
g = svn.walk("", srem, [], uname, permsets, True, scandir, True)
|
||||||
|
for dbv, vrem, _, atop, files, rd, vd in g:
|
||||||
|
if dbv != jail:
|
||||||
|
# the actual check (avoid toctou)
|
||||||
|
raise Pebkac(400, "mv: source folder contains other volumes")
|
||||||
|
|
||||||
|
for fn in files:
|
||||||
|
svpf = "/".join(x for x in [dbv.vpath, vrem, fn[0]] if x)
|
||||||
|
if not svpf.startswith(svp + "/"): # assert
|
||||||
|
raise Pebkac(500, "mv: bug at {}, top {}".format(svpf, svp))
|
||||||
|
|
||||||
|
dvpf = dvp + svpf[len(svp) :]
|
||||||
|
with self.mutex:
|
||||||
|
self._mv_file(uname, svpf, dvpf)
|
||||||
|
|
||||||
|
rmdirs(self.log_func, scandir, True, sabs)
|
||||||
|
return "k"
|
||||||
|
|
||||||
|
def _mv_file(self, uname, svp, dvp):
|
||||||
|
svn, srem = self.asrv.vfs.get(svp, uname, True, False, True)
|
||||||
|
svn, srem = svn.get_dbv(srem)
|
||||||
|
|
||||||
|
dvn, drem = self.asrv.vfs.get(dvp, uname, False, True)
|
||||||
|
dvn, drem = dvn.get_dbv(drem)
|
||||||
|
|
||||||
|
sabs = svn.canonical(srem, False)
|
||||||
|
dabs = dvn.canonical(drem)
|
||||||
|
drd, dfn = vsplit(drem)
|
||||||
|
|
||||||
|
n1 = svp.split("/")[-1]
|
||||||
|
n2 = dvp.split("/")[-1]
|
||||||
|
if n1.startswith(".") or n2.startswith("."):
|
||||||
|
if self.args.no_dot_mv:
|
||||||
|
raise Pebkac(400, "moving dotfiles is disabled in server config")
|
||||||
|
elif self.args.no_dot_ren and n1 != n2:
|
||||||
|
raise Pebkac(400, "renaming dotfiles is disabled in server config")
|
||||||
|
|
||||||
|
if bos.path.exists(dabs):
|
||||||
|
raise Pebkac(400, "mv2: target file exists")
|
||||||
|
|
||||||
|
bos.makedirs(os.path.dirname(dabs))
|
||||||
|
|
||||||
|
if bos.path.islink(sabs):
|
||||||
|
dlabs = absreal(sabs)
|
||||||
|
m = "moving symlink from [{}] to [{}], target [{}]"
|
||||||
|
self.log(m.format(sabs, dabs, dlabs))
|
||||||
|
os.unlink(sabs)
|
||||||
|
self._symlink(dlabs, dabs, False)
|
||||||
|
|
||||||
|
# folders are too scary, schedule rescan of both vols
|
||||||
|
self.need_rescan[svn.vpath] = 1
|
||||||
|
self.need_rescan[dvn.vpath] = 1
|
||||||
|
return "k"
|
||||||
|
|
||||||
|
c1, w, ftime, fsize, ip, at = self._find_from_vpath(svn.realpath, srem)
|
||||||
|
c2 = self.cur.get(dvn.realpath)
|
||||||
|
|
||||||
|
if ftime is None:
|
||||||
|
st = bos.stat(sabs)
|
||||||
|
ftime = st.st_mtime
|
||||||
|
fsize = st.st_size
|
||||||
|
|
||||||
|
if w:
|
||||||
|
if c2 and c2 != c1:
|
||||||
|
self._copy_tags(c1, c2, w)
|
||||||
|
|
||||||
|
self._forget_file(svn.realpath, srem, c1, w, c1 != c2)
|
||||||
|
self._relink(w, svn.realpath, srem, dabs)
|
||||||
|
c1.connection.commit()
|
||||||
|
|
||||||
|
if c2:
|
||||||
|
self.db_add(c2, w, drd, dfn, ftime, fsize, ip, at)
|
||||||
|
c2.connection.commit()
|
||||||
|
else:
|
||||||
|
self.log("not found in src db: [{}]".format(svp))
|
||||||
|
|
||||||
|
bos.rename(sabs, dabs)
|
||||||
|
return "k"
|
||||||
|
|
||||||
|
def _copy_tags(self, csrc, cdst, wark):
|
||||||
|
"""copy all tags for wark from src-db to dst-db"""
|
||||||
|
w = wark[:16]
|
||||||
|
|
||||||
|
if cdst.execute("select * from mt where w=? limit 1", (w,)).fetchone():
|
||||||
|
return # existing tags in dest db
|
||||||
|
|
||||||
|
for _, k, v in csrc.execute("select * from mt where w=?", (w,)):
|
||||||
|
cdst.execute("insert into mt values(?,?,?)", (w, k, v))
|
||||||
|
|
||||||
|
def _find_from_vpath(self, ptop, vrem):
|
||||||
|
cur = self.cur.get(ptop)
|
||||||
|
if not cur:
|
||||||
|
return [None] * 6
|
||||||
|
|
||||||
|
rd, fn = vsplit(vrem)
|
||||||
|
q = "select w, mt, sz, ip, at from up where rd=? and fn=? limit 1"
|
||||||
|
try:
|
||||||
|
c = cur.execute(q, (rd, fn))
|
||||||
|
except:
|
||||||
|
c = cur.execute(q, s3enc(self.mem_cur, rd, fn))
|
||||||
|
|
||||||
|
hit = c.fetchone()
|
||||||
|
if hit:
|
||||||
|
wark, ftime, fsize, ip, at = hit
|
||||||
|
return cur, wark, ftime, fsize, ip, at
|
||||||
|
return cur, None, None, None, None, None
|
||||||
|
|
||||||
|
def _forget_file(self, ptop, vrem, cur, wark, drop_tags):
|
||||||
|
"""forgets file in db, fixes symlinks, does not delete"""
|
||||||
|
srd, sfn = vsplit(vrem)
|
||||||
|
self.log("forgetting {}".format(vrem))
|
||||||
|
if wark:
|
||||||
|
self.log("found {} in db".format(wark))
|
||||||
|
if drop_tags:
|
||||||
|
if self._relink(wark, ptop, vrem, None):
|
||||||
|
drop_tags = False
|
||||||
|
|
||||||
|
if drop_tags:
|
||||||
|
q = "delete from mt where w=?"
|
||||||
|
cur.execute(q, (wark[:16],))
|
||||||
|
|
||||||
|
self.db_rm(cur, srd, sfn)
|
||||||
|
|
||||||
|
reg = self.registry.get(ptop)
|
||||||
|
if reg:
|
||||||
|
if not wark:
|
||||||
|
wark = [
|
||||||
|
x
|
||||||
|
for x, y in reg.items()
|
||||||
|
if fn in [y["name"], y.get("tnam")] and y["prel"] == vrem
|
||||||
|
]
|
||||||
|
|
||||||
|
if wark and wark in reg:
|
||||||
|
m = "forgetting partial upload {} ({})"
|
||||||
|
p = self._vis_job_progress(wark)
|
||||||
|
self.log(m.format(wark, p))
|
||||||
|
del reg[wark]
|
||||||
|
|
||||||
|
def _relink(self, wark, sptop, srem, dabs):
|
||||||
|
"""
|
||||||
|
update symlinks from file at svn/srem to dabs (rename),
|
||||||
|
or to first remaining full if no dabs (delete)
|
||||||
|
"""
|
||||||
|
dupes = []
|
||||||
|
sabs = os.path.join(sptop, srem)
|
||||||
|
q = "select rd, fn from up where substr(w,1,16)=? and w=?"
|
||||||
|
for ptop, cur in self.cur.items():
|
||||||
|
for rd, fn in cur.execute(q, (wark[:16], wark)):
|
||||||
|
if rd.startswith("//") or fn.startswith("//"):
|
||||||
|
rd, fn = s3dec(rd, fn)
|
||||||
|
|
||||||
|
dvrem = "/".join([rd, fn]).strip("/")
|
||||||
|
if ptop != sptop or srem != dvrem:
|
||||||
|
dupes.append([ptop, dvrem])
|
||||||
|
self.log("found {} dupe: [{}] {}".format(wark, ptop, dvrem))
|
||||||
|
|
||||||
|
if not dupes:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
full = {}
|
||||||
|
links = {}
|
||||||
|
for ptop, vp in dupes:
|
||||||
|
ap = os.path.join(ptop, vp)
|
||||||
|
try:
|
||||||
|
d = links if bos.path.islink(ap) else full
|
||||||
|
d[ap] = [ptop, vp]
|
||||||
|
except:
|
||||||
|
self.log("relink: not found: [{}]".format(ap))
|
||||||
|
|
||||||
|
if not dabs and not full and links:
|
||||||
|
# deleting final remaining full copy; swap it with a symlink
|
||||||
|
slabs = list(sorted(links.keys()))[0]
|
||||||
|
ptop, rem = links.pop(slabs)
|
||||||
|
self.log("linkswap [{}] and [{}]".format(sabs, slabs))
|
||||||
|
bos.unlink(slabs)
|
||||||
|
bos.rename(sabs, slabs)
|
||||||
|
self._symlink(slabs, sabs, False)
|
||||||
|
full[slabs] = [ptop, rem]
|
||||||
|
|
||||||
|
if not dabs:
|
||||||
|
dabs = list(sorted(full.keys()))[0]
|
||||||
|
|
||||||
|
for alink in links.keys():
|
||||||
|
try:
|
||||||
|
if alink != sabs and absreal(alink) != sabs:
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.log("relinking [{}] to [{}]".format(alink, dabs))
|
||||||
|
bos.unlink(alink)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
self._symlink(dabs, alink, False)
|
||||||
|
|
||||||
|
return len(full) + len(links)
|
||||||
|
|
||||||
def _get_wark(self, cj):
|
def _get_wark(self, cj):
|
||||||
if len(cj["name"]) > 1024 or len(cj["hash"]) > 512 * 1024: # 16TiB
|
if len(cj["name"]) > 1024 or len(cj["hash"]) > 512 * 1024: # 16TiB
|
||||||
raise Pebkac(400, "name or numchunks not according to spec")
|
raise Pebkac(400, "name or numchunks not according to spec")
|
||||||
@@ -1284,7 +1710,7 @@ class Up2k(object):
|
|||||||
|
|
||||||
def _hashlist_from_file(self, path):
|
def _hashlist_from_file(self, path):
|
||||||
pp = self.pp if hasattr(self, "pp") else None
|
pp = self.pp if hasattr(self, "pp") else None
|
||||||
fsz = os.path.getsize(fsenc(path))
|
fsz = bos.path.getsize(path)
|
||||||
csz = up2k_chunksize(fsz)
|
csz = up2k_chunksize(fsz)
|
||||||
ret = []
|
ret = []
|
||||||
with open(fsenc(path), "rb", 512 * 1024) as f:
|
with open(fsenc(path), "rb", 512 * 1024) as f:
|
||||||
@@ -1352,7 +1778,7 @@ class Up2k(object):
|
|||||||
for path, sz, times in ready:
|
for path, sz, times in ready:
|
||||||
self.log("lmod: setting times {} on {}".format(times, path))
|
self.log("lmod: setting times {} on {}".format(times, path))
|
||||||
try:
|
try:
|
||||||
os.utime(fsenc(path), times)
|
bos.utime(path, times)
|
||||||
except:
|
except:
|
||||||
self.log("lmod: failed to utime ({}, {})".format(path, times))
|
self.log("lmod: failed to utime ({}, {})".format(path, times))
|
||||||
|
|
||||||
@@ -1363,19 +1789,22 @@ class Up2k(object):
|
|||||||
self.log("could not unsparse [{}]".format(path), 3)
|
self.log("could not unsparse [{}]".format(path), 3)
|
||||||
|
|
||||||
def _snapshot(self):
|
def _snapshot(self):
|
||||||
persist_interval = 30 # persist unfinished uploads index every 30 sec
|
self.snap_persist_interval = 300 # persist unfinished index every 5 min
|
||||||
discard_interval = 21600 # drop unfinished uploads after 6 hours inactivity
|
self.snap_discard_interval = 21600 # drop unfinished after 6 hours inactivity
|
||||||
prev = {}
|
self.snap_prev = {}
|
||||||
while True:
|
while True:
|
||||||
time.sleep(persist_interval)
|
time.sleep(self.snap_persist_interval)
|
||||||
|
self.do_snapshot()
|
||||||
|
|
||||||
|
def do_snapshot(self):
|
||||||
with self.mutex:
|
with self.mutex:
|
||||||
for k, reg in self.registry.items():
|
for k, reg in self.registry.items():
|
||||||
self._snap_reg(prev, k, reg, discard_interval)
|
self._snap_reg(k, reg)
|
||||||
|
|
||||||
def _snap_reg(self, prev, ptop, reg, discard_interval):
|
def _snap_reg(self, ptop, reg):
|
||||||
now = time.time()
|
now = time.time()
|
||||||
histpath = self.asrv.vfs.histtab[ptop]
|
histpath = self.asrv.vfs.histtab[ptop]
|
||||||
rm = [x for x in reg.values() if now - x["poke"] > discard_interval]
|
rm = [x for x in reg.values() if now - x["poke"] > self.snap_discard_interval]
|
||||||
if rm:
|
if rm:
|
||||||
m = "dropping {} abandoned uploads in {}".format(len(rm), ptop)
|
m = "dropping {} abandoned uploads in {}".format(len(rm), ptop)
|
||||||
vis = [self._vis_job_progress(x) for x in rm]
|
vis = [self._vis_job_progress(x) for x in rm]
|
||||||
@@ -1385,33 +1814,30 @@ class Up2k(object):
|
|||||||
try:
|
try:
|
||||||
# remove the filename reservation
|
# remove the filename reservation
|
||||||
path = os.path.join(job["ptop"], job["prel"], job["name"])
|
path = os.path.join(job["ptop"], job["prel"], job["name"])
|
||||||
if os.path.getsize(fsenc(path)) == 0:
|
if bos.path.getsize(path) == 0:
|
||||||
os.unlink(fsenc(path))
|
bos.unlink(path)
|
||||||
|
|
||||||
if len(job["hash"]) == len(job["need"]):
|
if len(job["hash"]) == len(job["need"]):
|
||||||
# PARTIAL is empty, delete that too
|
# PARTIAL is empty, delete that too
|
||||||
path = os.path.join(job["ptop"], job["prel"], job["tnam"])
|
path = os.path.join(job["ptop"], job["prel"], job["tnam"])
|
||||||
os.unlink(fsenc(path))
|
bos.unlink(path)
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
path = os.path.join(histpath, "up2k.snap")
|
path = os.path.join(histpath, "up2k.snap")
|
||||||
if not reg:
|
if not reg:
|
||||||
if ptop not in prev or prev[ptop] is not None:
|
if ptop not in self.snap_prev or self.snap_prev[ptop] is not None:
|
||||||
prev[ptop] = None
|
self.snap_prev[ptop] = None
|
||||||
if os.path.exists(fsenc(path)):
|
if bos.path.exists(path):
|
||||||
os.unlink(fsenc(path))
|
bos.unlink(path)
|
||||||
return
|
return
|
||||||
|
|
||||||
newest = max(x["poke"] for _, x in reg.items()) if reg else 0
|
newest = max(x["poke"] for _, x in reg.items()) if reg else 0
|
||||||
etag = [len(reg), newest]
|
etag = [len(reg), newest]
|
||||||
if etag == prev.get(ptop):
|
if etag == self.snap_prev.get(ptop):
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
bos.makedirs(histpath)
|
||||||
os.makedirs(histpath)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
path2 = "{}.{}".format(path, os.getpid())
|
path2 = "{}.{}".format(path, os.getpid())
|
||||||
j = json.dumps(reg, indent=2, sort_keys=True).encode("utf-8")
|
j = json.dumps(reg, indent=2, sort_keys=True).encode("utf-8")
|
||||||
@@ -1421,7 +1847,7 @@ class Up2k(object):
|
|||||||
atomic_move(path2, path)
|
atomic_move(path2, path)
|
||||||
|
|
||||||
self.log("snap: {} |{}|".format(path, len(reg.keys())))
|
self.log("snap: {} |{}|".format(path, len(reg.keys())))
|
||||||
prev[ptop] = etag
|
self.snap_prev[ptop] = etag
|
||||||
|
|
||||||
def _tagger(self):
|
def _tagger(self):
|
||||||
with self.mutex:
|
with self.mutex:
|
||||||
@@ -1469,26 +1895,31 @@ class Up2k(object):
|
|||||||
self.n_hashq -= 1
|
self.n_hashq -= 1
|
||||||
# self.log("hashq {}".format(self.n_hashq))
|
# self.log("hashq {}".format(self.n_hashq))
|
||||||
|
|
||||||
ptop, rd, fn = self.hashq.get()
|
ptop, rd, fn, ip, at = self.hashq.get()
|
||||||
# self.log("hashq {} pop {}/{}/{}".format(self.n_hashq, ptop, rd, fn))
|
# self.log("hashq {} pop {}/{}/{}".format(self.n_hashq, ptop, rd, fn))
|
||||||
if "e2d" not in self.flags[ptop]:
|
if "e2d" not in self.flags[ptop]:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
abspath = os.path.join(ptop, rd, fn)
|
abspath = os.path.join(ptop, rd, fn)
|
||||||
self.log("hashing " + abspath)
|
self.log("hashing " + abspath)
|
||||||
inf = os.stat(fsenc(abspath))
|
inf = bos.stat(abspath)
|
||||||
hashes = self._hashlist_from_file(abspath)
|
hashes = self._hashlist_from_file(abspath)
|
||||||
wark = up2k_wark_from_hashlist(self.salt, inf.st_size, hashes)
|
wark = up2k_wark_from_hashlist(self.salt, inf.st_size, hashes)
|
||||||
with self.mutex:
|
with self.mutex:
|
||||||
self.idx_wark(ptop, wark, rd, fn, inf.st_mtime, inf.st_size)
|
self.idx_wark(ptop, wark, rd, fn, inf.st_mtime, inf.st_size, ip, at)
|
||||||
|
|
||||||
def hash_file(self, ptop, flags, rd, fn):
|
def hash_file(self, ptop, flags, rd, fn, ip, at):
|
||||||
with self.mutex:
|
with self.mutex:
|
||||||
self.register_vpath(ptop, flags)
|
self.register_vpath(ptop, flags)
|
||||||
self.hashq.put([ptop, rd, fn])
|
self.hashq.put([ptop, rd, fn, ip, at])
|
||||||
self.n_hashq += 1
|
self.n_hashq += 1
|
||||||
# self.log("hashq {} push {}/{}/{}".format(self.n_hashq, ptop, rd, fn))
|
# self.log("hashq {} push {}/{}/{}".format(self.n_hashq, ptop, rd, fn))
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
if hasattr(self, "snap_prev"):
|
||||||
|
self.log("writing snapshot")
|
||||||
|
self.do_snapshot()
|
||||||
|
|
||||||
|
|
||||||
def up2k_chunksize(filesize):
|
def up2k_chunksize(filesize):
|
||||||
chunksize = 1024 * 1024
|
chunksize = 1024 * 1024
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from __future__ import print_function, unicode_literals
|
|||||||
import re
|
import re
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import stat
|
||||||
import time
|
import time
|
||||||
import base64
|
import base64
|
||||||
import select
|
import select
|
||||||
@@ -16,8 +17,9 @@ import mimetypes
|
|||||||
import contextlib
|
import contextlib
|
||||||
import subprocess as sp # nosec
|
import subprocess as sp # nosec
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from collections import Counter
|
||||||
|
|
||||||
from .__init__ import PY2, WINDOWS, ANYWIN
|
from .__init__ import PY2, WINDOWS, ANYWIN, VT100
|
||||||
from .stolen import surrogateescape
|
from .stolen import surrogateescape
|
||||||
|
|
||||||
FAKE_MP = False
|
FAKE_MP = False
|
||||||
@@ -56,6 +58,9 @@ except:
|
|||||||
return struct.unpack(f.decode("ascii"), *a, **ka)
|
return struct.unpack(f.decode("ascii"), *a, **ka)
|
||||||
|
|
||||||
|
|
||||||
|
ansi_re = re.compile("\033\\[[^mK]*[mK]")
|
||||||
|
|
||||||
|
|
||||||
surrogateescape.register_surrogateescape()
|
surrogateescape.register_surrogateescape()
|
||||||
FS_ENCODING = sys.getfilesystemencoding()
|
FS_ENCODING = sys.getfilesystemencoding()
|
||||||
if WINDOWS and PY2:
|
if WINDOWS and PY2:
|
||||||
@@ -75,6 +80,7 @@ HTTPCODE = {
|
|||||||
403: "Forbidden",
|
403: "Forbidden",
|
||||||
404: "Not Found",
|
404: "Not Found",
|
||||||
405: "Method Not Allowed",
|
405: "Method Not Allowed",
|
||||||
|
411: "Length Required",
|
||||||
413: "Payload Too Large",
|
413: "Payload Too Large",
|
||||||
416: "Requested Range Not Satisfiable",
|
416: "Requested Range Not Satisfiable",
|
||||||
422: "Unprocessable Entity",
|
422: "Unprocessable Entity",
|
||||||
@@ -201,17 +207,22 @@ class ProgressPrinter(threading.Thread):
|
|||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
msg = None
|
msg = None
|
||||||
|
fmt = " {}\033[K\r" if VT100 else " {} $\r"
|
||||||
while not self.end:
|
while not self.end:
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
if msg == self.msg or self.end:
|
if msg == self.msg or self.end:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
msg = self.msg
|
msg = self.msg
|
||||||
uprint(" {}\033[K\r".format(msg))
|
uprint(fmt.format(msg))
|
||||||
if PY2:
|
if PY2:
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
if VT100:
|
||||||
print("\033[K", end="")
|
print("\033[K", end="")
|
||||||
|
elif msg:
|
||||||
|
print("------------------------")
|
||||||
|
|
||||||
sys.stdout.flush() # necessary on win10 even w/ stderr btw
|
sys.stdout.flush() # necessary on win10 even w/ stderr btw
|
||||||
|
|
||||||
|
|
||||||
@@ -282,15 +293,76 @@ def alltrace():
|
|||||||
return "\n".join(rret + bret)
|
return "\n".join(rret + bret)
|
||||||
|
|
||||||
|
|
||||||
|
def start_stackmon(arg_str, nid):
|
||||||
|
suffix = "-{}".format(nid) if nid else ""
|
||||||
|
fp, f = arg_str.rsplit(",", 1)
|
||||||
|
f = int(f)
|
||||||
|
t = threading.Thread(
|
||||||
|
target=stackmon,
|
||||||
|
args=(fp, f, suffix),
|
||||||
|
name="stackmon" + suffix,
|
||||||
|
)
|
||||||
|
t.daemon = True
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
|
||||||
|
def stackmon(fp, ival, suffix):
|
||||||
|
ctr = 0
|
||||||
|
while True:
|
||||||
|
ctr += 1
|
||||||
|
time.sleep(ival)
|
||||||
|
st = "{}, {}\n{}".format(ctr, time.time(), alltrace())
|
||||||
|
with open(fp + suffix, "wb") as f:
|
||||||
|
f.write(st.encode("utf-8", "replace"))
|
||||||
|
|
||||||
|
|
||||||
|
def start_log_thrs(logger, ival, nid):
|
||||||
|
ival = int(ival)
|
||||||
|
tname = lname = "log-thrs"
|
||||||
|
if nid:
|
||||||
|
tname = "logthr-n{}-i{:x}".format(nid, os.getpid())
|
||||||
|
lname = tname[3:]
|
||||||
|
|
||||||
|
t = threading.Thread(
|
||||||
|
target=log_thrs,
|
||||||
|
args=(logger, ival, lname),
|
||||||
|
name=tname,
|
||||||
|
)
|
||||||
|
t.daemon = True
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
|
||||||
|
def log_thrs(log, ival, name):
|
||||||
|
while True:
|
||||||
|
time.sleep(ival)
|
||||||
|
tv = [x.name for x in threading.enumerate()]
|
||||||
|
tv = [
|
||||||
|
x.split("-")[0]
|
||||||
|
if x.startswith("httpconn-") or x.startswith("thumb-")
|
||||||
|
else "listen"
|
||||||
|
if "-listen-" in x
|
||||||
|
else x
|
||||||
|
for x in tv
|
||||||
|
if not x.startswith("pydevd.")
|
||||||
|
]
|
||||||
|
tv = ["{}\033[36m{}".format(v, k) for k, v in sorted(Counter(tv).items())]
|
||||||
|
log(name, "\033[0m \033[33m".join(tv), 3)
|
||||||
|
|
||||||
|
|
||||||
|
def vol_san(vols, txt):
|
||||||
|
for vol in vols:
|
||||||
|
txt = txt.replace(vol.realpath.encode("utf-8"), vol.vpath.encode("utf-8"))
|
||||||
|
|
||||||
|
return txt
|
||||||
|
|
||||||
|
|
||||||
def min_ex():
|
def min_ex():
|
||||||
et, ev, tb = sys.exc_info()
|
et, ev, tb = sys.exc_info()
|
||||||
tb = traceback.extract_tb(tb, 2)
|
tb = traceback.extract_tb(tb)
|
||||||
ex = [
|
fmt = "{} @ {} <{}>: {}"
|
||||||
"{} @ {} <{}>: {}".format(fp.split(os.sep)[-1], ln, fun, txt)
|
ex = [fmt.format(fp.split(os.sep)[-1], ln, fun, txt) for fp, ln, fun, txt in tb]
|
||||||
for fp, ln, fun, txt in tb
|
ex.append("[{}] {}".format(et.__name__, ev))
|
||||||
]
|
return "\n".join(ex[-8:])
|
||||||
ex.append("{}: {}".format(et.__name__, ev))
|
|
||||||
return "\n".join(ex)
|
|
||||||
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
@@ -628,6 +700,17 @@ def humansize(sz, terse=False):
|
|||||||
return ret.replace("iB", "").replace(" ", "")
|
return ret.replace("iB", "").replace(" ", "")
|
||||||
|
|
||||||
|
|
||||||
|
def unhumanize(sz):
|
||||||
|
try:
|
||||||
|
return float(sz)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
mul = sz[-1:].lower()
|
||||||
|
mul = {"k": 1024, "m": 1024 * 1024, "g": 1024 * 1024 * 1024}.get(mul, 1)
|
||||||
|
return float(sz[:-1]) * mul
|
||||||
|
|
||||||
|
|
||||||
def get_spd(nbyte, t0, t=None):
|
def get_spd(nbyte, t0, t=None):
|
||||||
if t is None:
|
if t is None:
|
||||||
t = time.time()
|
t = time.time()
|
||||||
@@ -674,7 +757,7 @@ def undot(path):
|
|||||||
return "/".join(ret)
|
return "/".join(ret)
|
||||||
|
|
||||||
|
|
||||||
def sanitize_fn(fn, ok="", bad=[]):
|
def sanitize_fn(fn, ok, bad):
|
||||||
if "/" not in ok:
|
if "/" not in ok:
|
||||||
fn = fn.replace("\\", "/").split("/")[-1]
|
fn = fn.replace("\\", "/").split("/")[-1]
|
||||||
|
|
||||||
@@ -703,6 +786,19 @@ def sanitize_fn(fn, ok="", bad=[]):
|
|||||||
return fn.strip()
|
return fn.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def absreal(fpath):
|
||||||
|
try:
|
||||||
|
return fsdec(os.path.abspath(os.path.realpath(fsenc(fpath))))
|
||||||
|
except:
|
||||||
|
if not WINDOWS:
|
||||||
|
raise
|
||||||
|
|
||||||
|
# cpython bug introduced in 3.8, still exists in 3.9.1,
|
||||||
|
# some win7sp1 and win10:20H2 boxes cannot realpath a
|
||||||
|
# networked drive letter such as b"n:" or b"n:\\"
|
||||||
|
return os.path.abspath(os.path.realpath(fpath))
|
||||||
|
|
||||||
|
|
||||||
def u8safe(txt):
|
def u8safe(txt):
|
||||||
try:
|
try:
|
||||||
return txt.encode("utf-8", "xmlcharrefreplace").decode("utf-8", "replace")
|
return txt.encode("utf-8", "xmlcharrefreplace").decode("utf-8", "replace")
|
||||||
@@ -760,6 +856,13 @@ def unquotep(txt):
|
|||||||
return w8dec(unq2)
|
return w8dec(unq2)
|
||||||
|
|
||||||
|
|
||||||
|
def vsplit(vpath):
|
||||||
|
if "/" not in vpath:
|
||||||
|
return "", vpath
|
||||||
|
|
||||||
|
return vpath.rsplit("/", 1)
|
||||||
|
|
||||||
|
|
||||||
def w8dec(txt):
|
def w8dec(txt):
|
||||||
"""decodes filesystem-bytes to wtf8"""
|
"""decodes filesystem-bytes to wtf8"""
|
||||||
if PY2:
|
if PY2:
|
||||||
@@ -959,6 +1062,9 @@ def sendfile_kern(lower, upper, f, s):
|
|||||||
|
|
||||||
|
|
||||||
def statdir(logger, scandir, lstat, top):
|
def statdir(logger, scandir, lstat, top):
|
||||||
|
if lstat and not os.supports_follow_symlinks:
|
||||||
|
scandir = False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
btop = fsenc(top)
|
btop = fsenc(top)
|
||||||
if scandir and hasattr(os, "scandir"):
|
if scandir and hasattr(os, "scandir"):
|
||||||
@@ -983,6 +1089,29 @@ def statdir(logger, scandir, lstat, top):
|
|||||||
logger(src, "{} @ {}".format(repr(ex), top), 1)
|
logger(src, "{} @ {}".format(repr(ex), top), 1)
|
||||||
|
|
||||||
|
|
||||||
|
def rmdirs(logger, scandir, lstat, top):
|
||||||
|
if not os.path.exists(fsenc(top)) or not os.path.isdir(fsenc(top)):
|
||||||
|
top = os.path.dirname(top)
|
||||||
|
|
||||||
|
dirs = statdir(logger, scandir, lstat, top)
|
||||||
|
dirs = [x[0] for x in dirs if stat.S_ISDIR(x[1].st_mode)]
|
||||||
|
dirs = [os.path.join(top, x) for x in dirs]
|
||||||
|
ok = []
|
||||||
|
ng = []
|
||||||
|
for d in dirs[::-1]:
|
||||||
|
a, b = rmdirs(logger, scandir, lstat, d)
|
||||||
|
ok += a
|
||||||
|
ng += b
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.rmdir(fsenc(top))
|
||||||
|
ok.append(top)
|
||||||
|
except:
|
||||||
|
ng.append(top)
|
||||||
|
|
||||||
|
return ok, ng
|
||||||
|
|
||||||
|
|
||||||
def unescape_cookie(orig):
|
def unescape_cookie(orig):
|
||||||
# mw=idk; doot=qwe%2Crty%3Basd+fgh%2Bjkl%25zxc%26vbn # qwe,rty;asd fgh+jkl%zxc&vbn
|
# mw=idk; doot=qwe%2Crty%3Basd+fgh%2Bjkl%25zxc%26vbn # qwe,rty;asd fgh+jkl%zxc&vbn
|
||||||
ret = ""
|
ret = ""
|
||||||
@@ -1026,7 +1155,7 @@ def guess_mime(url, fallback="application/octet-stream"):
|
|||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
def runcmd(*argv):
|
def runcmd(argv):
|
||||||
p = sp.Popen(argv, stdout=sp.PIPE, stderr=sp.PIPE)
|
p = sp.Popen(argv, stdout=sp.PIPE, stderr=sp.PIPE)
|
||||||
stdout, stderr = p.communicate()
|
stdout, stderr = p.communicate()
|
||||||
stdout = stdout.decode("utf-8", "replace")
|
stdout = stdout.decode("utf-8", "replace")
|
||||||
@@ -1034,8 +1163,8 @@ def runcmd(*argv):
|
|||||||
return [p.returncode, stdout, stderr]
|
return [p.returncode, stdout, stderr]
|
||||||
|
|
||||||
|
|
||||||
def chkcmd(*argv):
|
def chkcmd(argv):
|
||||||
ok, sout, serr = runcmd(*argv)
|
ok, sout, serr = runcmd(argv)
|
||||||
if ok != 0:
|
if ok != 0:
|
||||||
raise Exception(serr)
|
raise Exception(serr)
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ window.baguetteBox = (function () {
|
|||||||
captions: true,
|
captions: true,
|
||||||
buttons: 'auto',
|
buttons: 'auto',
|
||||||
noScrollbars: false,
|
noScrollbars: false,
|
||||||
bodyClass: 'baguetteBox-open',
|
bodyClass: 'bbox-open',
|
||||||
titleTag: false,
|
titleTag: false,
|
||||||
async: false,
|
async: false,
|
||||||
preload: 2,
|
preload: 2,
|
||||||
@@ -22,37 +22,46 @@ window.baguetteBox = (function () {
|
|||||||
afterHide: null,
|
afterHide: null,
|
||||||
onChange: null,
|
onChange: null,
|
||||||
},
|
},
|
||||||
overlay, slider, previousButton, nextButton, closeButton,
|
overlay, slider, btnPrev, btnNext, btnHelp, btnRotL, btnRotR, btnSel, btnVmode, btnClose,
|
||||||
currentGallery = [],
|
currentGallery = [],
|
||||||
currentIndex = 0,
|
currentIndex = 0,
|
||||||
isOverlayVisible = false,
|
isOverlayVisible = false,
|
||||||
touch = {}, // start-pos
|
touch = {}, // start-pos
|
||||||
touchFlag = false, // busy
|
touchFlag = false, // busy
|
||||||
regex = /.+\.(gif|jpe?g|png|webp)/i,
|
re_i = /.+\.(gif|jpe?g|png|webp)(\?|$)/i,
|
||||||
|
re_v = /.+\.(webm|mp4)(\?|$)/i,
|
||||||
data = {}, // all galleries
|
data = {}, // all galleries
|
||||||
imagesElements = [],
|
imagesElements = [],
|
||||||
documentLastFocus = null;
|
documentLastFocus = null,
|
||||||
|
isFullscreen = false,
|
||||||
|
vmute = false,
|
||||||
|
vloop = false,
|
||||||
|
vnext = false,
|
||||||
|
resume_mp = false;
|
||||||
|
|
||||||
var overlayClickHandler = function (event) {
|
var onFSC = function (e) {
|
||||||
if (event.target.id.indexOf('baguette-img') !== -1) {
|
isFullscreen = !!document.fullscreenElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
var overlayClickHandler = function (e) {
|
||||||
|
if (e.target.id.indexOf('baguette-img') !== -1)
|
||||||
hideOverlay();
|
hideOverlay();
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
var touchstartHandler = function (event) {
|
var touchstartHandler = function (e) {
|
||||||
touch.count++;
|
touch.count = e.touches.length;
|
||||||
if (touch.count > 1) {
|
if (touch.count > 1)
|
||||||
touch.multitouch = true;
|
touch.multitouch = true;
|
||||||
}
|
|
||||||
touch.startX = event.changedTouches[0].pageX;
|
touch.startX = e.changedTouches[0].pageX;
|
||||||
touch.startY = event.changedTouches[0].pageY;
|
touch.startY = e.changedTouches[0].pageY;
|
||||||
};
|
};
|
||||||
var touchmoveHandler = function (event) {
|
var touchmoveHandler = function (e) {
|
||||||
if (touchFlag || touch.multitouch) {
|
if (touchFlag || touch.multitouch)
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
event.preventDefault ? event.preventDefault() : event.returnValue = false;
|
e.preventDefault ? e.preventDefault() : e.returnValue = false;
|
||||||
var touchEvent = event.touches[0] || event.changedTouches[0];
|
var touchEvent = e.touches[0] || e.changedTouches[0];
|
||||||
if (touchEvent.pageX - touch.startX > 40) {
|
if (touchEvent.pageX - touch.startX > 40) {
|
||||||
touchFlag = true;
|
touchFlag = true;
|
||||||
showPreviousImage();
|
showPreviousImage();
|
||||||
@@ -63,21 +72,24 @@ window.baguetteBox = (function () {
|
|||||||
hideOverlay();
|
hideOverlay();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
var touchendHandler = function () {
|
var touchendHandler = function (e) {
|
||||||
touch.count--;
|
touch.count--;
|
||||||
if (touch.count <= 0) {
|
if (e && e.touches)
|
||||||
|
touch.count = e.touches.length;
|
||||||
|
|
||||||
|
if (touch.count <= 0)
|
||||||
touch.multitouch = false;
|
touch.multitouch = false;
|
||||||
}
|
|
||||||
touchFlag = false;
|
touchFlag = false;
|
||||||
};
|
};
|
||||||
var contextmenuHandler = function () {
|
var contextmenuHandler = function () {
|
||||||
touchendHandler();
|
touchendHandler();
|
||||||
};
|
};
|
||||||
|
|
||||||
var trapFocusInsideOverlay = function (event) {
|
var trapFocusInsideOverlay = function (e) {
|
||||||
if (overlay.style.display === 'block' && (overlay.contains && !overlay.contains(event.target))) {
|
if (overlay.style.display === 'block' && (overlay.contains && !overlay.contains(e.target))) {
|
||||||
event.stopPropagation();
|
e.stopPropagation();
|
||||||
initFocus();
|
btnClose.focus();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -88,7 +100,7 @@ window.baguetteBox = (function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function bindImageClickListeners(selector, userOptions) {
|
function bindImageClickListeners(selector, userOptions) {
|
||||||
var galleryNodeList = document.querySelectorAll(selector);
|
var galleryNodeList = QSA(selector);
|
||||||
var selectorData = {
|
var selectorData = {
|
||||||
galleries: [],
|
galleries: [],
|
||||||
nodeList: galleryNodeList
|
nodeList: galleryNodeList
|
||||||
@@ -96,33 +108,26 @@ window.baguetteBox = (function () {
|
|||||||
data[selector] = selectorData;
|
data[selector] = selectorData;
|
||||||
|
|
||||||
[].forEach.call(galleryNodeList, function (galleryElement) {
|
[].forEach.call(galleryNodeList, function (galleryElement) {
|
||||||
if (userOptions && userOptions.filter) {
|
|
||||||
regex = userOptions.filter;
|
|
||||||
}
|
|
||||||
|
|
||||||
var tagsNodeList = [];
|
var tagsNodeList = [];
|
||||||
if (galleryElement.tagName === 'A') {
|
if (galleryElement.tagName === 'A')
|
||||||
tagsNodeList = [galleryElement];
|
tagsNodeList = [galleryElement];
|
||||||
} else {
|
else
|
||||||
tagsNodeList = galleryElement.getElementsByTagName('a');
|
tagsNodeList = galleryElement.getElementsByTagName('a');
|
||||||
}
|
|
||||||
|
|
||||||
tagsNodeList = [].filter.call(tagsNodeList, function (element) {
|
tagsNodeList = [].filter.call(tagsNodeList, function (element) {
|
||||||
if (element.className.indexOf(userOptions && userOptions.ignoreClass) === -1) {
|
if (element.className.indexOf(userOptions && userOptions.ignoreClass) === -1)
|
||||||
return regex.test(element.href);
|
return re_i.test(element.href) || re_v.test(element.href);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
if (tagsNodeList.length === 0) {
|
if (!tagsNodeList.length)
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
var gallery = [];
|
var gallery = [];
|
||||||
[].forEach.call(tagsNodeList, function (imageElement, imageIndex) {
|
[].forEach.call(tagsNodeList, function (imageElement, imageIndex) {
|
||||||
var imageElementClickHandler = function (event) {
|
var imageElementClickHandler = function (e) {
|
||||||
if (event && (event.ctrlKey || event.metaKey))
|
if (ctrl(e))
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
event.preventDefault ? event.preventDefault() : event.returnValue = false;
|
e.preventDefault ? e.preventDefault() : e.returnValue = false;
|
||||||
prepareOverlay(gallery, userOptions);
|
prepareOverlay(gallery, userOptions);
|
||||||
showOverlay(imageIndex);
|
showOverlay(imageIndex);
|
||||||
};
|
};
|
||||||
@@ -140,93 +145,232 @@ window.baguetteBox = (function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function clearCachedData() {
|
function clearCachedData() {
|
||||||
for (var selector in data) {
|
for (var selector in data)
|
||||||
if (data.hasOwnProperty(selector)) {
|
if (data.hasOwnProperty(selector))
|
||||||
removeFromCache(selector);
|
removeFromCache(selector);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeFromCache(selector) {
|
function removeFromCache(selector) {
|
||||||
if (!data.hasOwnProperty(selector)) {
|
if (!data.hasOwnProperty(selector))
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
var galleries = data[selector].galleries;
|
var galleries = data[selector].galleries;
|
||||||
[].forEach.call(galleries, function (gallery) {
|
[].forEach.call(galleries, function (gallery) {
|
||||||
[].forEach.call(gallery, function (imageItem) {
|
[].forEach.call(gallery, function (imageItem) {
|
||||||
unbind(imageItem.imageElement, 'click', imageItem.eventHandler);
|
unbind(imageItem.imageElement, 'click', imageItem.eventHandler);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (currentGallery === gallery) {
|
if (currentGallery === gallery)
|
||||||
currentGallery = [];
|
currentGallery = [];
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
delete data[selector];
|
delete data[selector];
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildOverlay() {
|
function buildOverlay() {
|
||||||
overlay = ebi('baguetteBox-overlay');
|
overlay = ebi('bbox-overlay');
|
||||||
if (overlay) {
|
if (!overlay) {
|
||||||
slider = ebi('baguetteBox-slider');
|
var ctr = mknod('div');
|
||||||
previousButton = ebi('previous-button');
|
ctr.innerHTML = (
|
||||||
nextButton = ebi('next-button');
|
'<div id="bbox-overlay" role="dialog">' +
|
||||||
closeButton = ebi('close-button');
|
'<div id="bbox-slider"></div>' +
|
||||||
return;
|
'<button id="bbox-prev" class="bbox-btn" type="button" aria-label="Previous"><</button>' +
|
||||||
|
'<button id="bbox-next" class="bbox-btn" type="button" aria-label="Next">></button>' +
|
||||||
|
'<div id="bbox-btns">' +
|
||||||
|
'<button id="bbox-help" type="button">?</button>' +
|
||||||
|
'<button id="bbox-rotl" type="button">↶</button>' +
|
||||||
|
'<button id="bbox-rotr" type="button">↷</button>' +
|
||||||
|
'<button id="bbox-tsel" type="button">sel</button>' +
|
||||||
|
'<button id="bbox-vmode" type="button" tt="a"></button>' +
|
||||||
|
'<button id="bbox-close" type="button" aria-label="Close">X</button>' +
|
||||||
|
'</div></div>'
|
||||||
|
);
|
||||||
|
overlay = ctr.firstChild;
|
||||||
|
QS('body').appendChild(overlay);
|
||||||
|
tt.att(overlay);
|
||||||
}
|
}
|
||||||
overlay = mknod('div');
|
slider = ebi('bbox-slider');
|
||||||
overlay.setAttribute('role', 'dialog');
|
btnPrev = ebi('bbox-prev');
|
||||||
overlay.id = 'baguetteBox-overlay';
|
btnNext = ebi('bbox-next');
|
||||||
document.getElementsByTagName('body')[0].appendChild(overlay);
|
btnHelp = ebi('bbox-help');
|
||||||
|
btnRotL = ebi('bbox-rotl');
|
||||||
slider = mknod('div');
|
btnRotR = ebi('bbox-rotr');
|
||||||
slider.id = 'baguetteBox-slider';
|
btnSel = ebi('bbox-tsel');
|
||||||
overlay.appendChild(slider);
|
btnVmode = ebi('bbox-vmode');
|
||||||
|
btnClose = ebi('bbox-close');
|
||||||
previousButton = mknod('button');
|
|
||||||
previousButton.setAttribute('type', 'button');
|
|
||||||
previousButton.id = 'previous-button';
|
|
||||||
previousButton.setAttribute('aria-label', 'Previous');
|
|
||||||
previousButton.innerHTML = '<';
|
|
||||||
overlay.appendChild(previousButton);
|
|
||||||
|
|
||||||
nextButton = mknod('button');
|
|
||||||
nextButton.setAttribute('type', 'button');
|
|
||||||
nextButton.id = 'next-button';
|
|
||||||
nextButton.setAttribute('aria-label', 'Next');
|
|
||||||
nextButton.innerHTML = '>';
|
|
||||||
overlay.appendChild(nextButton);
|
|
||||||
|
|
||||||
closeButton = mknod('button');
|
|
||||||
closeButton.setAttribute('type', 'button');
|
|
||||||
closeButton.id = 'close-button';
|
|
||||||
closeButton.setAttribute('aria-label', 'Close');
|
|
||||||
closeButton.innerHTML = '×';
|
|
||||||
overlay.appendChild(closeButton);
|
|
||||||
|
|
||||||
previousButton.className = nextButton.className = closeButton.className = 'baguetteBox-button';
|
|
||||||
|
|
||||||
bindEvents();
|
bindEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
function keyDownHandler(event) {
|
function halp() {
|
||||||
switch (event.keyCode) {
|
if (ebi('bbox-halp'))
|
||||||
case 37: // Left
|
return;
|
||||||
showPreviousImage();
|
|
||||||
break;
|
var list = [
|
||||||
case 39: // Right
|
['<b># hotkey</b>', '<b># operation</b>'],
|
||||||
showNextImage();
|
['escape', 'close'],
|
||||||
break;
|
['left, J', 'previous file'],
|
||||||
case 27: // Esc
|
['right, L', 'next file'],
|
||||||
hideOverlay();
|
['home', 'first file'],
|
||||||
break;
|
['end', 'last file'],
|
||||||
case 36: // Home
|
['R', 'rotate (shift=ccw)'],
|
||||||
showFirstImage(event);
|
['S', 'toggle file selection'],
|
||||||
break;
|
['space, P, K', 'video: play / pause'],
|
||||||
case 35: // End
|
['U', 'video: seek 10sec back'],
|
||||||
showLastImage(event);
|
['P', 'video: seek 10sec ahead'],
|
||||||
break;
|
['M', 'video: toggle mute'],
|
||||||
|
['V', 'video: toggle loop'],
|
||||||
|
['C', 'video: toggle auto-next'],
|
||||||
|
['F', 'video: toggle fullscreen'],
|
||||||
|
],
|
||||||
|
d = mknod('table'),
|
||||||
|
html = ['<tbody>'];
|
||||||
|
|
||||||
|
for (var a = 0; a < list.length; a++)
|
||||||
|
html.push('<tr><td>' + list[a][0] + '</td><td>' + list[a][1] + '</td></tr>');
|
||||||
|
|
||||||
|
d.innerHTML = html.join('\n') + '</tbody>';
|
||||||
|
d.setAttribute('id', 'bbox-halp');
|
||||||
|
d.onclick = function () {
|
||||||
|
overlay.removeChild(d);
|
||||||
|
};
|
||||||
|
overlay.appendChild(d);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function keyDownHandler(e) {
|
||||||
|
if (e.ctrlKey || e.altKey || e.metaKey || e.isComposing)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var k = e.code + '', v = vid();
|
||||||
|
|
||||||
|
if (k == "ArrowLeft" || k == "KeyJ")
|
||||||
|
showPreviousImage();
|
||||||
|
else if (k == "ArrowRight" || k == "KeyL")
|
||||||
|
showNextImage();
|
||||||
|
else if (k == "Escape")
|
||||||
|
hideOverlay();
|
||||||
|
else if (k == "Home")
|
||||||
|
showFirstImage(e);
|
||||||
|
else if (k == "End")
|
||||||
|
showLastImage(e);
|
||||||
|
else if (k == "Space" || k == "KeyP" || k == "KeyK")
|
||||||
|
playpause();
|
||||||
|
else if (k == "KeyU" || k == "KeyO")
|
||||||
|
relseek(k == "KeyU" ? -10 : 10);
|
||||||
|
else if (k == "KeyM" && v) {
|
||||||
|
v.muted = vmute = !vmute;
|
||||||
|
mp_ctl();
|
||||||
|
}
|
||||||
|
else if (k == "KeyV" && v) {
|
||||||
|
vloop = !vloop;
|
||||||
|
vnext = vnext && !vloop;
|
||||||
|
setVmode();
|
||||||
|
}
|
||||||
|
else if (k == "KeyC" && v) {
|
||||||
|
vnext = !vnext;
|
||||||
|
vloop = vloop && !vnext;
|
||||||
|
setVmode();
|
||||||
|
}
|
||||||
|
else if (k == "KeyF")
|
||||||
|
try {
|
||||||
|
if (isFullscreen)
|
||||||
|
document.exitFullscreen();
|
||||||
|
else
|
||||||
|
v.requestFullscreen();
|
||||||
|
}
|
||||||
|
catch (ex) { }
|
||||||
|
else if (k == "KeyS")
|
||||||
|
tglsel();
|
||||||
|
else if (k == "KeyR")
|
||||||
|
rotn(e.shiftKey ? -1 : 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setVmode() {
|
||||||
|
var v = vid();
|
||||||
|
ebi('bbox-vmode').style.display = v ? '' : 'none';
|
||||||
|
if (!v)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var msg = 'When video ends, ', tts = '', lbl;
|
||||||
|
if (vloop) {
|
||||||
|
lbl = 'Loop';
|
||||||
|
msg += 'repeat it';
|
||||||
|
tts = '$NHotkey: V';
|
||||||
|
}
|
||||||
|
else if (vnext) {
|
||||||
|
lbl = 'Cont';
|
||||||
|
msg += 'continue to next';
|
||||||
|
tts = '$NHotkey: C';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
lbl = 'Stop';
|
||||||
|
msg += 'just stop'
|
||||||
|
}
|
||||||
|
btnVmode.setAttribute('aria-label', msg);
|
||||||
|
btnVmode.setAttribute('tt', msg + tts);
|
||||||
|
btnVmode.textContent = lbl;
|
||||||
|
|
||||||
|
v.loop = vloop
|
||||||
|
if (vloop && v.paused)
|
||||||
|
v.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
function tglVmode() {
|
||||||
|
if (vloop) {
|
||||||
|
vnext = true;
|
||||||
|
vloop = false;
|
||||||
|
}
|
||||||
|
else if (vnext)
|
||||||
|
vnext = false;
|
||||||
|
else
|
||||||
|
vloop = true;
|
||||||
|
|
||||||
|
setVmode();
|
||||||
|
if (tt.en)
|
||||||
|
tt.show.bind(this)();
|
||||||
|
}
|
||||||
|
|
||||||
|
function tglsel() {
|
||||||
|
var thumb = currentGallery[currentIndex].imageElement,
|
||||||
|
name = vsplit(thumb.href)[1],
|
||||||
|
files = msel.getall();
|
||||||
|
|
||||||
|
for (var a = 0; a < files.length; a++)
|
||||||
|
if (vsplit(files[a].vp)[1] == name)
|
||||||
|
clmod(ebi(files[a].id).closest('tr'), 'sel', 't');
|
||||||
|
|
||||||
|
msel.selui();
|
||||||
|
selbg();
|
||||||
|
}
|
||||||
|
|
||||||
|
function selbg() {
|
||||||
|
var img = vidimg(),
|
||||||
|
thumb = currentGallery[currentIndex].imageElement,
|
||||||
|
name = vsplit(thumb.href)[1],
|
||||||
|
files = msel.getsel(),
|
||||||
|
sel = false;
|
||||||
|
|
||||||
|
for (var a = 0; a < files.length; a++)
|
||||||
|
if (vsplit(files[a].vp)[1] == name)
|
||||||
|
sel = true;
|
||||||
|
|
||||||
|
ebi('bbox-overlay').style.background = sel ?
|
||||||
|
'rgba(153,34,85,0.7)' : '';
|
||||||
|
|
||||||
|
img.style.borderRadius = sel ? '1em' : '';
|
||||||
|
btnSel.style.color = sel ? '#fff' : '';
|
||||||
|
btnSel.style.background = sel ? '#d48' : '';
|
||||||
|
btnSel.style.textShadow = sel ? '1px 1px 0 #b38' : '';
|
||||||
|
btnSel.style.boxShadow = sel ? '.15em .15em 0 #502' : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function keyUpHandler(e) {
|
||||||
|
if (e.ctrlKey || e.altKey || e.metaKey || e.isComposing)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var k = e.code + '';
|
||||||
|
|
||||||
|
if (k == "Space")
|
||||||
|
ev(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
var passiveSupp = false;
|
var passiveSupp = false;
|
||||||
@@ -248,9 +392,14 @@ window.baguetteBox = (function () {
|
|||||||
|
|
||||||
function bindEvents() {
|
function bindEvents() {
|
||||||
bind(overlay, 'click', overlayClickHandler);
|
bind(overlay, 'click', overlayClickHandler);
|
||||||
bind(previousButton, 'click', showPreviousImage);
|
bind(btnPrev, 'click', showPreviousImage);
|
||||||
bind(nextButton, 'click', showNextImage);
|
bind(btnNext, 'click', showNextImage);
|
||||||
bind(closeButton, 'click', hideOverlay);
|
bind(btnClose, 'click', hideOverlay);
|
||||||
|
bind(btnVmode, 'click', tglVmode);
|
||||||
|
bind(btnHelp, 'click', halp);
|
||||||
|
bind(btnRotL, 'click', rotl);
|
||||||
|
bind(btnRotR, 'click', rotr);
|
||||||
|
bind(btnSel, 'click', tglsel);
|
||||||
bind(slider, 'contextmenu', contextmenuHandler);
|
bind(slider, 'contextmenu', contextmenuHandler);
|
||||||
bind(overlay, 'touchstart', touchstartHandler, nonPassiveEvent);
|
bind(overlay, 'touchstart', touchstartHandler, nonPassiveEvent);
|
||||||
bind(overlay, 'touchmove', touchmoveHandler, passiveEvent);
|
bind(overlay, 'touchmove', touchmoveHandler, passiveEvent);
|
||||||
@@ -260,20 +409,26 @@ window.baguetteBox = (function () {
|
|||||||
|
|
||||||
function unbindEvents() {
|
function unbindEvents() {
|
||||||
unbind(overlay, 'click', overlayClickHandler);
|
unbind(overlay, 'click', overlayClickHandler);
|
||||||
unbind(previousButton, 'click', showPreviousImage);
|
unbind(btnPrev, 'click', showPreviousImage);
|
||||||
unbind(nextButton, 'click', showNextImage);
|
unbind(btnNext, 'click', showNextImage);
|
||||||
unbind(closeButton, 'click', hideOverlay);
|
unbind(btnClose, 'click', hideOverlay);
|
||||||
|
unbind(btnVmode, 'click', tglVmode);
|
||||||
|
unbind(btnHelp, 'click', halp);
|
||||||
|
unbind(btnRotL, 'click', rotl);
|
||||||
|
unbind(btnRotR, 'click', rotr);
|
||||||
|
unbind(btnSel, 'click', tglsel);
|
||||||
unbind(slider, 'contextmenu', contextmenuHandler);
|
unbind(slider, 'contextmenu', contextmenuHandler);
|
||||||
unbind(overlay, 'touchstart', touchstartHandler, nonPassiveEvent);
|
unbind(overlay, 'touchstart', touchstartHandler, nonPassiveEvent);
|
||||||
unbind(overlay, 'touchmove', touchmoveHandler, passiveEvent);
|
unbind(overlay, 'touchmove', touchmoveHandler, passiveEvent);
|
||||||
unbind(overlay, 'touchend', touchendHandler);
|
unbind(overlay, 'touchend', touchendHandler);
|
||||||
unbind(document, 'focus', trapFocusInsideOverlay, true);
|
unbind(document, 'focus', trapFocusInsideOverlay, true);
|
||||||
|
timer.rm(rotn);
|
||||||
}
|
}
|
||||||
|
|
||||||
function prepareOverlay(gallery, userOptions) {
|
function prepareOverlay(gallery, userOptions) {
|
||||||
if (currentGallery === gallery) {
|
if (currentGallery === gallery)
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
currentGallery = gallery;
|
currentGallery = gallery;
|
||||||
setOptions(userOptions);
|
setOptions(userOptions);
|
||||||
slider.innerHTML = '';
|
slider.innerHTML = '';
|
||||||
@@ -287,8 +442,8 @@ window.baguetteBox = (function () {
|
|||||||
fullImage.id = 'baguette-img-' + i;
|
fullImage.id = 'baguette-img-' + i;
|
||||||
imagesElements.push(fullImage);
|
imagesElements.push(fullImage);
|
||||||
|
|
||||||
imagesFiguresIds.push('baguetteBox-figure-' + i);
|
imagesFiguresIds.push('bbox-figure-' + i);
|
||||||
imagesCaptionsIds.push('baguetteBox-figcaption-' + i);
|
imagesCaptionsIds.push('bbox-figcaption-' + i);
|
||||||
slider.appendChild(imagesElements[i]);
|
slider.appendChild(imagesElements[i]);
|
||||||
}
|
}
|
||||||
overlay.setAttribute('aria-labelledby', imagesFiguresIds.join(' '));
|
overlay.setAttribute('aria-labelledby', imagesFiguresIds.join(' '));
|
||||||
@@ -296,23 +451,21 @@ window.baguetteBox = (function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setOptions(newOptions) {
|
function setOptions(newOptions) {
|
||||||
if (!newOptions) {
|
if (!newOptions)
|
||||||
newOptions = {};
|
newOptions = {};
|
||||||
}
|
|
||||||
for (var item in defaults) {
|
for (var item in defaults) {
|
||||||
options[item] = defaults[item];
|
options[item] = defaults[item];
|
||||||
if (typeof newOptions[item] !== 'undefined') {
|
if (typeof newOptions[item] !== 'undefined')
|
||||||
options[item] = newOptions[item];
|
options[item] = newOptions[item];
|
||||||
}
|
}
|
||||||
}
|
|
||||||
slider.style.transition = (options.animation === 'fadeIn' ? 'opacity .4s ease' :
|
slider.style.transition = (options.animation === 'fadeIn' ? 'opacity .4s ease' :
|
||||||
options.animation === 'slideIn' ? '' : 'none');
|
options.animation === 'slideIn' ? '' : 'none');
|
||||||
|
|
||||||
if (options.buttons === 'auto' && ('ontouchstart' in window || currentGallery.length === 1)) {
|
if (options.buttons === 'auto' && ('ontouchstart' in window || currentGallery.length === 1))
|
||||||
options.buttons = false;
|
options.buttons = false;
|
||||||
}
|
|
||||||
|
|
||||||
previousButton.style.display = nextButton.style.display = (options.buttons ? '' : 'none');
|
btnPrev.style.display = btnNext.style.display = (options.buttons ? '' : 'none');
|
||||||
}
|
}
|
||||||
|
|
||||||
function showOverlay(chosenImageIndex) {
|
function showOverlay(chosenImageIndex) {
|
||||||
@@ -320,11 +473,12 @@ window.baguetteBox = (function () {
|
|||||||
document.documentElement.style.overflowY = 'hidden';
|
document.documentElement.style.overflowY = 'hidden';
|
||||||
document.body.style.overflowY = 'scroll';
|
document.body.style.overflowY = 'scroll';
|
||||||
}
|
}
|
||||||
if (overlay.style.display === 'block') {
|
if (overlay.style.display === 'block')
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
bind(document, 'keydown', keyDownHandler);
|
bind(document, 'keydown', keyDownHandler);
|
||||||
|
bind(document, 'keyup', keyUpHandler);
|
||||||
|
bind(document, 'fullscreenchange', onFSC);
|
||||||
currentIndex = chosenImageIndex;
|
currentIndex = chosenImageIndex;
|
||||||
touch = {
|
touch = {
|
||||||
count: 0,
|
count: 0,
|
||||||
@@ -341,50 +495,48 @@ window.baguetteBox = (function () {
|
|||||||
// Fade in overlay
|
// Fade in overlay
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
overlay.className = 'visible';
|
overlay.className = 'visible';
|
||||||
if (options.bodyClass && document.body.classList) {
|
if (options.bodyClass && document.body.classList)
|
||||||
document.body.classList.add(options.bodyClass);
|
document.body.classList.add(options.bodyClass);
|
||||||
}
|
|
||||||
if (options.afterShow) {
|
|
||||||
options.afterShow();
|
|
||||||
}
|
|
||||||
}, 50);
|
|
||||||
if (options.onChange) {
|
|
||||||
options.onChange(currentIndex, imagesElements.length);
|
|
||||||
}
|
|
||||||
documentLastFocus = document.activeElement;
|
|
||||||
initFocus();
|
|
||||||
isOverlayVisible = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function initFocus() {
|
if (options.afterShow)
|
||||||
if (options.buttons) {
|
options.afterShow();
|
||||||
previousButton.focus();
|
}, 50);
|
||||||
} else {
|
|
||||||
closeButton.focus();
|
if (options.onChange)
|
||||||
}
|
options.onChange(currentIndex, imagesElements.length);
|
||||||
|
|
||||||
|
documentLastFocus = document.activeElement;
|
||||||
|
btnClose.focus();
|
||||||
|
isOverlayVisible = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideOverlay(e) {
|
function hideOverlay(e) {
|
||||||
ev(e);
|
ev(e);
|
||||||
|
playvid(false);
|
||||||
if (options.noScrollbars) {
|
if (options.noScrollbars) {
|
||||||
document.documentElement.style.overflowY = 'auto';
|
document.documentElement.style.overflowY = 'auto';
|
||||||
document.body.style.overflowY = 'auto';
|
document.body.style.overflowY = 'auto';
|
||||||
}
|
}
|
||||||
if (overlay.style.display === 'none') {
|
if (overlay.style.display === 'none')
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
unbind(document, 'keydown', keyDownHandler);
|
unbind(document, 'keydown', keyDownHandler);
|
||||||
|
unbind(document, 'keyup', keyUpHandler);
|
||||||
|
unbind(document, 'fullscreenchange', onFSC);
|
||||||
// Fade out and hide the overlay
|
// Fade out and hide the overlay
|
||||||
overlay.className = '';
|
overlay.className = '';
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
overlay.style.display = 'none';
|
overlay.style.display = 'none';
|
||||||
if (options.bodyClass && document.body.classList) {
|
if (options.bodyClass && document.body.classList)
|
||||||
document.body.classList.remove(options.bodyClass);
|
document.body.classList.remove(options.bodyClass);
|
||||||
}
|
|
||||||
if (options.afterHide) {
|
var h = ebi('bbox-halp');
|
||||||
|
if (h)
|
||||||
|
h.parentNode.removeChild(h);
|
||||||
|
|
||||||
|
if (options.afterHide)
|
||||||
options.afterHide();
|
options.afterHide();
|
||||||
}
|
|
||||||
documentLastFocus && documentLastFocus.focus();
|
documentLastFocus && documentLastFocus.focus();
|
||||||
isOverlayVisible = false;
|
isOverlayVisible = false;
|
||||||
}, 500);
|
}, 500);
|
||||||
@@ -394,60 +546,69 @@ window.baguetteBox = (function () {
|
|||||||
var imageContainer = imagesElements[index];
|
var imageContainer = imagesElements[index];
|
||||||
var galleryItem = currentGallery[index];
|
var galleryItem = currentGallery[index];
|
||||||
|
|
||||||
if (typeof imageContainer === 'undefined' || typeof galleryItem === 'undefined') {
|
if (typeof imageContainer === 'undefined' || typeof galleryItem === 'undefined')
|
||||||
return; // out-of-bounds or gallery dirty
|
return; // out-of-bounds or gallery dirty
|
||||||
}
|
|
||||||
|
|
||||||
if (imageContainer.getElementsByTagName('img')[0]) {
|
if (imageContainer.querySelector('img, video'))
|
||||||
// image is loaded, cb and bail
|
// was loaded, cb and bail
|
||||||
if (callback) {
|
return callback ? callback() : null;
|
||||||
callback();
|
|
||||||
}
|
// maybe unloaded video
|
||||||
return;
|
while (imageContainer.firstChild)
|
||||||
}
|
imageContainer.removeChild(imageContainer.firstChild);
|
||||||
|
|
||||||
var imageElement = galleryItem.imageElement,
|
var imageElement = galleryItem.imageElement,
|
||||||
imageSrc = imageElement.href,
|
imageSrc = imageElement.href,
|
||||||
thumbnailElement = imageElement.getElementsByTagName('img')[0],
|
is_vid = re_v.test(imageSrc),
|
||||||
|
thumbnailElement = imageElement.querySelector('img, video'),
|
||||||
imageCaption = typeof options.captions === 'function' ?
|
imageCaption = typeof options.captions === 'function' ?
|
||||||
options.captions.call(currentGallery, imageElement) :
|
options.captions.call(currentGallery, imageElement) :
|
||||||
imageElement.getAttribute('data-caption') || imageElement.title;
|
imageElement.getAttribute('data-caption') || imageElement.title;
|
||||||
|
|
||||||
|
imageSrc += imageSrc.indexOf('?') < 0 ? '?cache' : '&cache';
|
||||||
|
|
||||||
|
if (is_vid && index != currentIndex)
|
||||||
|
return; // no preload
|
||||||
|
|
||||||
var figure = mknod('figure');
|
var figure = mknod('figure');
|
||||||
figure.id = 'baguetteBox-figure-' + index;
|
figure.id = 'bbox-figure-' + index;
|
||||||
figure.innerHTML = '<div class="baguetteBox-spinner">' +
|
figure.innerHTML = '<div class="bbox-spinner">' +
|
||||||
'<div class="baguetteBox-double-bounce1"></div>' +
|
'<div class="bbox-double-bounce1"></div>' +
|
||||||
'<div class="baguetteBox-double-bounce2"></div>' +
|
'<div class="bbox-double-bounce2"></div>' +
|
||||||
'</div>';
|
'</div>';
|
||||||
|
|
||||||
if (options.captions && imageCaption) {
|
if (options.captions && imageCaption) {
|
||||||
var figcaption = mknod('figcaption');
|
var figcaption = mknod('figcaption');
|
||||||
figcaption.id = 'baguetteBox-figcaption-' + index;
|
figcaption.id = 'bbox-figcaption-' + index;
|
||||||
figcaption.innerHTML = imageCaption;
|
figcaption.innerHTML = imageCaption;
|
||||||
figure.appendChild(figcaption);
|
figure.appendChild(figcaption);
|
||||||
}
|
}
|
||||||
imageContainer.appendChild(figure);
|
imageContainer.appendChild(figure);
|
||||||
|
|
||||||
var image = mknod('img');
|
var image = mknod(is_vid ? 'video' : 'img');
|
||||||
image.onload = function () {
|
clmod(imageContainer, 'vid', is_vid);
|
||||||
|
|
||||||
|
image.addEventListener(is_vid ? 'loadedmetadata' : 'load', function () {
|
||||||
// Remove loader element
|
// Remove loader element
|
||||||
var spinner = document.querySelector('#baguette-img-' + index + ' .baguetteBox-spinner');
|
var spinner = QS('#baguette-img-' + index + ' .bbox-spinner');
|
||||||
figure.removeChild(spinner);
|
figure.removeChild(spinner);
|
||||||
if (!options.async && callback) {
|
if (!options.async && callback)
|
||||||
callback();
|
callback();
|
||||||
}
|
});
|
||||||
};
|
|
||||||
image.setAttribute('src', imageSrc);
|
image.setAttribute('src', imageSrc);
|
||||||
image.alt = thumbnailElement ? thumbnailElement.alt || '' : '';
|
if (is_vid) {
|
||||||
if (options.titleTag && imageCaption) {
|
image.setAttribute('controls', 'controls');
|
||||||
image.title = imageCaption;
|
image.onended = vidEnd;
|
||||||
}
|
}
|
||||||
|
image.alt = thumbnailElement ? thumbnailElement.alt || '' : '';
|
||||||
|
if (options.titleTag && imageCaption)
|
||||||
|
image.title = imageCaption;
|
||||||
|
|
||||||
figure.appendChild(image);
|
figure.appendChild(image);
|
||||||
|
|
||||||
if (options.async && callback) {
|
if (options.async && callback)
|
||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function showNextImage(e) {
|
function showNextImage(e) {
|
||||||
ev(e);
|
ev(e);
|
||||||
@@ -459,26 +620,20 @@ window.baguetteBox = (function () {
|
|||||||
return show(currentIndex - 1);
|
return show(currentIndex - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function showFirstImage(event) {
|
function showFirstImage(e) {
|
||||||
if (event) {
|
if (e)
|
||||||
event.preventDefault();
|
e.preventDefault();
|
||||||
}
|
|
||||||
return show(0);
|
return show(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function showLastImage(event) {
|
function showLastImage(e) {
|
||||||
if (event) {
|
if (e)
|
||||||
event.preventDefault();
|
e.preventDefault();
|
||||||
}
|
|
||||||
return show(currentGallery.length - 1);
|
return show(currentGallery.length - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Move the gallery to a specific index
|
|
||||||
* @param `index` {number} - the position of the image
|
|
||||||
* @param `gallery` {array} - gallery which should be opened, if omitted assumes the currently opened one
|
|
||||||
* @return {boolean} - true on success or false if the index is invalid
|
|
||||||
*/
|
|
||||||
function show(index, gallery) {
|
function show(index, gallery) {
|
||||||
if (!isOverlayVisible && index >= 0 && index < gallery.length) {
|
if (!isOverlayVisible && index >= 0 && index < gallery.length) {
|
||||||
prepareOverlay(gallery, options);
|
prepareOverlay(gallery, options);
|
||||||
@@ -486,18 +641,25 @@ window.baguetteBox = (function () {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (index < 0) {
|
if (index < 0) {
|
||||||
if (options.animation) {
|
if (options.animation)
|
||||||
bounceAnimation('left');
|
bounceAnimation('left');
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (index >= imagesElements.length) {
|
if (index >= imagesElements.length) {
|
||||||
if (options.animation) {
|
if (options.animation)
|
||||||
bounceAnimation('right');
|
bounceAnimation('right');
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var v = vid();
|
||||||
|
if (v) {
|
||||||
|
v.src = '';
|
||||||
|
v.load();
|
||||||
|
v.parentNode.removeChild(v);
|
||||||
|
}
|
||||||
|
|
||||||
currentIndex = index;
|
currentIndex = index;
|
||||||
loadImage(currentIndex, function () {
|
loadImage(currentIndex, function () {
|
||||||
preloadNext(currentIndex);
|
preloadNext(currentIndex);
|
||||||
@@ -505,17 +667,130 @@ window.baguetteBox = (function () {
|
|||||||
});
|
});
|
||||||
updateOffset();
|
updateOffset();
|
||||||
|
|
||||||
if (options.onChange) {
|
if (options.onChange)
|
||||||
options.onChange(currentIndex, imagesElements.length);
|
options.onChange(currentIndex, imagesElements.length);
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
var prev_cw = 0, prev_ch = 0, unrot_timer = null;
|
||||||
* Triggers the bounce animation
|
function rotn(n) {
|
||||||
* @param {('left'|'right')} direction - Direction of the movement
|
var el = vidimg(),
|
||||||
*/
|
orot = parseInt(el.getAttribute('rot') || 0),
|
||||||
|
frot = orot + (n || 0) * 90;
|
||||||
|
|
||||||
|
if (!frot && !orot)
|
||||||
|
return; // reflow noop
|
||||||
|
|
||||||
|
var co = ebi('bbox-overlay'),
|
||||||
|
cw = co.clientWidth,
|
||||||
|
ch = co.clientHeight;
|
||||||
|
|
||||||
|
if (!n && prev_cw === cw && prev_ch === ch)
|
||||||
|
return; // reflow noop
|
||||||
|
|
||||||
|
prev_cw = cw;
|
||||||
|
prev_ch = ch;
|
||||||
|
var rot = frot,
|
||||||
|
iw = el.naturalWidth || el.videoWidth,
|
||||||
|
ih = el.naturalHeight || el.videoHeight,
|
||||||
|
magic = 4, // idk, works in enough browsers
|
||||||
|
dl = el.closest('div').querySelector('figcaption a'),
|
||||||
|
vw = cw,
|
||||||
|
vh = ch - dl.offsetHeight + magic,
|
||||||
|
pmag = Math.min(1, Math.min(vw / ih, vh / iw)),
|
||||||
|
wmag = Math.min(1, Math.min(vw / iw, vh / ih));
|
||||||
|
|
||||||
|
while (rot < 0) rot += 360;
|
||||||
|
while (rot >= 360) rot -= 360;
|
||||||
|
var q = rot == 90 || rot == 270 ? 1 : 0,
|
||||||
|
mag = q ? pmag : wmag;
|
||||||
|
|
||||||
|
el.style.cssText = 'max-width:none; max-height:none; position:absolute; display:block; margin:0';
|
||||||
|
if (!orot) {
|
||||||
|
el.style.width = iw * wmag + 'px';
|
||||||
|
el.style.height = ih * wmag + 'px';
|
||||||
|
el.style.left = (vw - iw * wmag) / 2 + 'px';
|
||||||
|
el.style.top = (vh - ih * wmag) / 2 - magic + 'px';
|
||||||
|
q = el.offsetHeight;
|
||||||
|
}
|
||||||
|
el.style.width = iw * mag + 'px';
|
||||||
|
el.style.height = ih * mag + 'px';
|
||||||
|
el.style.left = (vw - iw * mag) / 2 + 'px';
|
||||||
|
el.style.top = (vh - ih * mag) / 2 - magic + 'px';
|
||||||
|
el.style.transform = 'rotate(' + frot + 'deg)';
|
||||||
|
el.setAttribute('rot', frot);
|
||||||
|
timer.add(rotn);
|
||||||
|
if (!rot) {
|
||||||
|
clearTimeout(unrot_timer);
|
||||||
|
unrot_timer = setTimeout(unrot, 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function rotl() {
|
||||||
|
rotn(-1);
|
||||||
|
}
|
||||||
|
function rotr() {
|
||||||
|
rotn(1);
|
||||||
|
}
|
||||||
|
function unrot() {
|
||||||
|
var el = vidimg(),
|
||||||
|
orot = el.getAttribute('rot'),
|
||||||
|
rot = parseInt(orot || 0);
|
||||||
|
|
||||||
|
while (rot < 0) rot += 360;
|
||||||
|
while (rot >= 360) rot -= 360;
|
||||||
|
if (rot || orot === null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
clmod(el, 'nt', 1);
|
||||||
|
el.removeAttribute('rot');
|
||||||
|
el.removeAttribute("style");
|
||||||
|
rot = el.offsetHeight;
|
||||||
|
clmod(el, 'nt');
|
||||||
|
timer.rm(rotn);
|
||||||
|
}
|
||||||
|
|
||||||
|
function vid() {
|
||||||
|
return imagesElements[currentIndex].querySelector('video');
|
||||||
|
}
|
||||||
|
|
||||||
|
function vidimg() {
|
||||||
|
return imagesElements[currentIndex].querySelector('img, video');
|
||||||
|
}
|
||||||
|
|
||||||
|
function playvid(play) {
|
||||||
|
if (vid())
|
||||||
|
vid()[play ? 'play' : 'pause']();
|
||||||
|
}
|
||||||
|
|
||||||
|
function playpause() {
|
||||||
|
var v = vid();
|
||||||
|
if (v)
|
||||||
|
v[v.paused ? "play" : "pause"]();
|
||||||
|
}
|
||||||
|
|
||||||
|
function relseek(sec) {
|
||||||
|
if (vid())
|
||||||
|
vid().currentTime += sec;
|
||||||
|
}
|
||||||
|
|
||||||
|
function vidEnd() {
|
||||||
|
if (this == vid() && vnext)
|
||||||
|
showNextImage();
|
||||||
|
}
|
||||||
|
|
||||||
|
function mp_ctl() {
|
||||||
|
var v = vid();
|
||||||
|
if (!vmute && v && mp.au && !mp.au.paused) {
|
||||||
|
mp.fade_out();
|
||||||
|
resume_mp = true;
|
||||||
|
}
|
||||||
|
else if (resume_mp && (vmute || !v) && mp.au && mp.au.paused) {
|
||||||
|
mp.fade_in();
|
||||||
|
resume_mp = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function bounceAnimation(direction) {
|
function bounceAnimation(direction) {
|
||||||
slider.className = 'bounce-from-' + direction;
|
slider.className = 'bounce-from-' + direction;
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
@@ -524,31 +799,59 @@ window.baguetteBox = (function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateOffset() {
|
function updateOffset() {
|
||||||
var offset = -currentIndex * 100 + '%';
|
var offset = -currentIndex * 100 + '%',
|
||||||
|
xform = slider.style.perspective !== undefined;
|
||||||
|
|
||||||
if (options.animation === 'fadeIn') {
|
if (options.animation === 'fadeIn') {
|
||||||
slider.style.opacity = 0;
|
slider.style.opacity = 0;
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
slider.style.transform = 'translate3d(' + offset + ',0,0)';
|
xform ?
|
||||||
|
slider.style.transform = 'translate3d(' + offset + ',0,0)' :
|
||||||
|
slider.style.left = offset;
|
||||||
slider.style.opacity = 1;
|
slider.style.opacity = 1;
|
||||||
}, 400);
|
}, 400);
|
||||||
} else {
|
} else {
|
||||||
slider.style.transform = 'translate3d(' + offset + ',0,0)';
|
xform ?
|
||||||
|
slider.style.transform = 'translate3d(' + offset + ',0,0)' :
|
||||||
|
slider.style.left = offset;
|
||||||
}
|
}
|
||||||
|
playvid(false);
|
||||||
|
var v = vid();
|
||||||
|
if (v) {
|
||||||
|
playvid(true);
|
||||||
|
v.muted = vmute;
|
||||||
|
v.loop = vloop;
|
||||||
|
}
|
||||||
|
selbg();
|
||||||
|
mp_ctl();
|
||||||
|
setVmode();
|
||||||
|
|
||||||
|
var el = vidimg();
|
||||||
|
if (el.getAttribute('rot'))
|
||||||
|
timer.add(rotn);
|
||||||
|
else
|
||||||
|
timer.rm(rotn);
|
||||||
|
|
||||||
|
var prev = QS('.full-image.vis');
|
||||||
|
if (prev)
|
||||||
|
clmod(prev, 'vis');
|
||||||
|
|
||||||
|
clmod(el.closest('div'), 'vis', 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function preloadNext(index) {
|
function preloadNext(index) {
|
||||||
if (index - currentIndex >= options.preload) {
|
if (index - currentIndex >= options.preload)
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
loadImage(index + 1, function () {
|
loadImage(index + 1, function () {
|
||||||
preloadNext(index + 1);
|
preloadNext(index + 1);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function preloadPrev(index) {
|
function preloadPrev(index) {
|
||||||
if (currentIndex - index >= options.preload) {
|
if (currentIndex - index >= options.preload)
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
loadImage(index - 1, function () {
|
loadImage(index - 1, function () {
|
||||||
preloadPrev(index - 1);
|
preloadPrev(index - 1);
|
||||||
});
|
});
|
||||||
@@ -566,7 +869,8 @@ window.baguetteBox = (function () {
|
|||||||
unbindEvents();
|
unbindEvents();
|
||||||
clearCachedData();
|
clearCachedData();
|
||||||
unbind(document, 'keydown', keyDownHandler);
|
unbind(document, 'keydown', keyDownHandler);
|
||||||
document.getElementsByTagName('body')[0].removeChild(ebi('baguetteBox-overlay'));
|
unbind(document, 'keyup', keyUpHandler);
|
||||||
|
document.getElementsByTagName('body')[0].removeChild(ebi('bbox-overlay'));
|
||||||
data = {};
|
data = {};
|
||||||
currentGallery = [];
|
currentGallery = [];
|
||||||
currentIndex = 0;
|
currentIndex = 0;
|
||||||
@@ -577,6 +881,8 @@ window.baguetteBox = (function () {
|
|||||||
show: show,
|
show: show,
|
||||||
showNext: showNextImage,
|
showNext: showNextImage,
|
||||||
showPrevious: showPreviousImage,
|
showPrevious: showPreviousImage,
|
||||||
|
relseek: relseek,
|
||||||
|
playpause: playpause,
|
||||||
hide: hideOverlay,
|
hide: hideOverlay,
|
||||||
destroy: destroyPlugin
|
destroy: destroyPlugin
|
||||||
};
|
};
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -6,10 +6,10 @@
|
|||||||
<title>⇆🎉 {{ title }}</title>
|
<title>⇆🎉 {{ title }}</title>
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=0.8">
|
<meta name="viewport" content="width=device-width, initial-scale=0.8">
|
||||||
<link rel="stylesheet" type="text/css" media="screen" href="/.cpr/browser.css?_={{ ts }}">
|
<link rel="stylesheet" media="screen" href="/.cpr/ui.css?_={{ ts }}">
|
||||||
<link rel="stylesheet" type="text/css" media="screen" href="/.cpr/upload.css?_={{ ts }}">
|
<link rel="stylesheet" media="screen" href="/.cpr/browser.css?_={{ ts }}">
|
||||||
{%- if css %}
|
{%- if css %}
|
||||||
<link rel="stylesheet" type="text/css" media="screen" href="{{ css }}?_={{ ts }}">
|
<link rel="stylesheet" media="screen" href="{{ css }}?_={{ ts }}">
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@@ -39,32 +39,34 @@
|
|||||||
<div id="op_mkdir" class="opview opbox act">
|
<div id="op_mkdir" class="opview opbox act">
|
||||||
<form method="post" enctype="multipart/form-data" accept-charset="utf-8" action="{{ url_suf }}">
|
<form method="post" enctype="multipart/form-data" accept-charset="utf-8" action="{{ url_suf }}">
|
||||||
<input type="hidden" name="act" value="mkdir" />
|
<input type="hidden" name="act" value="mkdir" />
|
||||||
<input type="text" name="name" size="30">
|
📂<input type="text" name="name" size="30">
|
||||||
<input type="submit" value="mkdir">
|
<input type="submit" value="make directory">
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="op_new_md" class="opview opbox">
|
<div id="op_new_md" class="opview opbox">
|
||||||
<form method="post" enctype="multipart/form-data" accept-charset="utf-8" action="{{ url_suf }}">
|
<form method="post" enctype="multipart/form-data" accept-charset="utf-8" action="{{ url_suf }}">
|
||||||
<input type="hidden" name="act" value="new_md" />
|
<input type="hidden" name="act" value="new_md" />
|
||||||
<input type="text" name="name" size="30">
|
📝<input type="text" name="name" size="30">
|
||||||
<input type="submit" value="create doc">
|
<input type="submit" value="new markdown doc">
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="op_msg" class="opview opbox act">
|
<div id="op_msg" class="opview opbox act">
|
||||||
<form method="post" enctype="application/x-www-form-urlencoded" accept-charset="utf-8" action="{{ url_suf }}">
|
<form method="post" enctype="application/x-www-form-urlencoded" accept-charset="utf-8" action="{{ url_suf }}">
|
||||||
<input type="text" name="msg" size="30">
|
📟<input type="text" name="msg" size="30">
|
||||||
<input type="submit" value="send msg">
|
<input type="submit" value="send msg to server log">
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="op_unpost" class="opview opbox"></div>
|
||||||
|
|
||||||
<div id="op_up2k" class="opview"></div>
|
<div id="op_up2k" class="opview"></div>
|
||||||
|
|
||||||
<div id="op_cfg" class="opview opbox opwide"></div>
|
<div id="op_cfg" class="opview opbox opwide"></div>
|
||||||
|
|
||||||
<h1 id="path">
|
<h1 id="path">
|
||||||
<a href="#" id="entree" tt="show directory tree$NHotkey: B">🌲</a>
|
<a href="#" id="entree" tt="show navpane (directory tree sidebar)$NHotkey: B">🌲</a>
|
||||||
{%- for n in vpnodes %}
|
{%- for n in vpnodes %}
|
||||||
<a href="/{{ n[0] }}">{{ n[1] }}</a>
|
<a href="/{{ n[0] }}">{{ n[1] }}</a>
|
||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
@@ -112,6 +114,8 @@
|
|||||||
|
|
||||||
<h2><a href="/?h">control-panel</a></h2>
|
<h2><a href="/?h">control-panel</a></h2>
|
||||||
|
|
||||||
|
<a href="#" id="repl">π</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{%- if srv_info %}
|
{%- if srv_info %}
|
||||||
@@ -121,11 +125,16 @@
|
|||||||
<div id="widget"></div>
|
<div id="widget"></div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
var perms = {{ perms }},
|
var acct = "{{ acct }}",
|
||||||
tag_order_cfg = {{ tag_order }},
|
perms = {{ perms }},
|
||||||
|
def_hcols = {{ def_hcols|tojson }},
|
||||||
have_up2k_idx = {{ have_up2k_idx|tojson }},
|
have_up2k_idx = {{ have_up2k_idx|tojson }},
|
||||||
have_tags_idx = {{ have_tags_idx|tojson }},
|
have_tags_idx = {{ have_tags_idx|tojson }},
|
||||||
have_zip = {{ have_zip|tojson }};
|
have_mv = {{ have_mv|tojson }},
|
||||||
|
have_del = {{ have_del|tojson }},
|
||||||
|
have_unpost = {{ have_unpost|tojson }},
|
||||||
|
have_zip = {{ have_zip|tojson }},
|
||||||
|
readme = {{ readme|tojson }};
|
||||||
</script>
|
</script>
|
||||||
<script src="/.cpr/util.js?_={{ ts }}"></script>
|
<script src="/.cpr/util.js?_={{ ts }}"></script>
|
||||||
<script src="/.cpr/browser.js?_={{ ts }}"></script>
|
<script src="/.cpr/browser.js?_={{ ts }}"></script>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,13 +1,17 @@
|
|||||||
@font-face {
|
|
||||||
font-family: 'scp';
|
|
||||||
src: local('Source Code Pro Regular'), local('SourceCodePro-Regular'), url(/.cpr/deps/scp.woff2) format('woff2');
|
|
||||||
}
|
|
||||||
html, body {
|
html, body {
|
||||||
color: #333;
|
color: #333;
|
||||||
background: #eee;
|
background: #eee;
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
line-height: 1.5em;
|
line-height: 1.5em;
|
||||||
}
|
}
|
||||||
|
#repl {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: .5em;
|
||||||
|
border: none;
|
||||||
|
color: inherit;
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
#mtw {
|
#mtw {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -15,122 +19,12 @@ html, body {
|
|||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 0 1.5em;
|
padding: 0 1.5em;
|
||||||
}
|
}
|
||||||
pre, code, a {
|
#toast {
|
||||||
color: #480;
|
bottom: auto;
|
||||||
background: #f7f7f7;
|
top: 1.4em;
|
||||||
border: .07em solid #ddd;
|
|
||||||
border-radius: .2em;
|
|
||||||
padding: .1em .3em;
|
|
||||||
margin: 0 .1em;
|
|
||||||
}
|
}
|
||||||
code {
|
a {
|
||||||
font-size: .96em;
|
text-decoration: none;
|
||||||
}
|
|
||||||
pre, code {
|
|
||||||
font-family: 'scp', monospace, monospace;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
pre {
|
|
||||||
counter-reset: precode;
|
|
||||||
}
|
|
||||||
pre code {
|
|
||||||
counter-increment: precode;
|
|
||||||
display: inline-block;
|
|
||||||
margin: 0 -.3em;
|
|
||||||
padding: .4em .5em;
|
|
||||||
border: none;
|
|
||||||
border-bottom: 1px solid #cdc;
|
|
||||||
min-width: calc(100% - .6em);
|
|
||||||
line-height: 1.1em;
|
|
||||||
}
|
|
||||||
pre code:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
pre code::before {
|
|
||||||
content: counter(precode);
|
|
||||||
-webkit-user-select: none;
|
|
||||||
-moz-user-select: none;
|
|
||||||
-ms-user-select: none;
|
|
||||||
user-select: none;
|
|
||||||
display: inline-block;
|
|
||||||
text-align: right;
|
|
||||||
font-size: .75em;
|
|
||||||
color: #48a;
|
|
||||||
width: 4em;
|
|
||||||
padding-right: 1.5em;
|
|
||||||
margin-left: -5.5em;
|
|
||||||
}
|
|
||||||
pre code:hover {
|
|
||||||
background: #fec;
|
|
||||||
color: #360;
|
|
||||||
}
|
|
||||||
h1, h2 {
|
|
||||||
line-height: 1.5em;
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
font-size: 1.7em;
|
|
||||||
text-align: center;
|
|
||||||
border: 1em solid #777;
|
|
||||||
border-width: .05em 0;
|
|
||||||
margin: 3em 0;
|
|
||||||
}
|
|
||||||
h2 {
|
|
||||||
font-size: 1.5em;
|
|
||||||
font-weight: normal;
|
|
||||||
background: #f7f7f7;
|
|
||||||
border-top: .07em solid #fff;
|
|
||||||
border-bottom: .07em solid #bbb;
|
|
||||||
border-radius: .5em .5em 0 0;
|
|
||||||
padding-left: .4em;
|
|
||||||
margin-top: 3em;
|
|
||||||
}
|
|
||||||
h3 {
|
|
||||||
border-bottom: .1em solid #999;
|
|
||||||
}
|
|
||||||
h1 a, h3 a, h5 a,
|
|
||||||
h2 a, h4 a, h6 a {
|
|
||||||
color: inherit;
|
|
||||||
display: block;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
#mp ul,
|
|
||||||
#mp ol {
|
|
||||||
border-left: .3em solid #ddd;
|
|
||||||
}
|
|
||||||
#m>ul,
|
|
||||||
#m>ol {
|
|
||||||
border-color: #bbb;
|
|
||||||
}
|
|
||||||
#mp ul>li {
|
|
||||||
list-style-type: disc;
|
|
||||||
}
|
|
||||||
#mp ul>li,
|
|
||||||
#mp ol>li {
|
|
||||||
margin: .7em 0;
|
|
||||||
}
|
|
||||||
strong {
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
p>em,
|
|
||||||
li>em,
|
|
||||||
td>em {
|
|
||||||
color: #c50;
|
|
||||||
padding: .1em;
|
|
||||||
border-bottom: .1em solid #bbb;
|
|
||||||
}
|
|
||||||
blockquote {
|
|
||||||
font-family: serif;
|
|
||||||
background: #f7f7f7;
|
|
||||||
border: .07em dashed #ccc;
|
|
||||||
padding: 0 2em;
|
|
||||||
margin: 1em 0;
|
|
||||||
}
|
|
||||||
small {
|
|
||||||
opacity: .8;
|
|
||||||
}
|
}
|
||||||
#toc {
|
#toc {
|
||||||
margin: 0 1em;
|
margin: 0 1em;
|
||||||
@@ -166,7 +60,7 @@ small {
|
|||||||
z-index: 99;
|
z-index: 99;
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-family: monospace, monospace;
|
font-family: 'scp', monospace, monospace;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 1.3em;
|
font-size: 1.3em;
|
||||||
line-height: .1em;
|
line-height: .1em;
|
||||||
@@ -178,14 +72,6 @@ small {
|
|||||||
color: #6b3;
|
color: #6b3;
|
||||||
text-shadow: .02em 0 0 #6b3;
|
text-shadow: .02em 0 0 #6b3;
|
||||||
}
|
}
|
||||||
table {
|
|
||||||
border-collapse: collapse;
|
|
||||||
margin: 1em 0;
|
|
||||||
}
|
|
||||||
th, td {
|
|
||||||
padding: .2em .5em;
|
|
||||||
border: .12em solid #aaa;
|
|
||||||
}
|
|
||||||
blink {
|
blink {
|
||||||
animation: blinker .7s cubic-bezier(.9, 0, .1, 1) infinite;
|
animation: blinker .7s cubic-bezier(.9, 0, .1, 1) infinite;
|
||||||
}
|
}
|
||||||
@@ -198,6 +84,36 @@ blink {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.mdo pre {
|
||||||
|
counter-reset: precode;
|
||||||
|
}
|
||||||
|
.mdo pre code {
|
||||||
|
counter-increment: precode;
|
||||||
|
display: inline-block;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid #cdc;
|
||||||
|
min-width: calc(100% - .6em);
|
||||||
|
}
|
||||||
|
.mdo pre code:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.mdo pre code::before {
|
||||||
|
content: counter(precode);
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
display: inline-block;
|
||||||
|
text-align: right;
|
||||||
|
font-size: .75em;
|
||||||
|
color: #48a;
|
||||||
|
width: 4em;
|
||||||
|
padding-right: 1.5em;
|
||||||
|
margin-left: -5.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@media screen {
|
@media screen {
|
||||||
html, body {
|
html, body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -214,34 +130,6 @@ blink {
|
|||||||
#mp {
|
#mp {
|
||||||
max-width: 52em;
|
max-width: 52em;
|
||||||
margin-bottom: 6em;
|
margin-bottom: 6em;
|
||||||
word-break: break-word;
|
|
||||||
overflow-wrap: break-word;
|
|
||||||
word-wrap: break-word; /*ie*/
|
|
||||||
}
|
|
||||||
a {
|
|
||||||
color: #fff;
|
|
||||||
background: #39b;
|
|
||||||
text-decoration: none;
|
|
||||||
padding: 0 .3em;
|
|
||||||
border: none;
|
|
||||||
border-bottom: .07em solid #079;
|
|
||||||
}
|
|
||||||
h2 {
|
|
||||||
color: #fff;
|
|
||||||
background: #555;
|
|
||||||
margin-top: 2em;
|
|
||||||
border-bottom: .22em solid #999;
|
|
||||||
border-top: none;
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
color: #fff;
|
|
||||||
background: #444;
|
|
||||||
font-weight: normal;
|
|
||||||
border-top: .4em solid #fb0;
|
|
||||||
border-bottom: .4em solid #777;
|
|
||||||
border-radius: 0 1em 0 1em;
|
|
||||||
margin: 3em 0 1em 0;
|
|
||||||
padding: .5em 0;
|
|
||||||
}
|
}
|
||||||
#mn {
|
#mn {
|
||||||
padding: 1.3em 0 .7em 1em;
|
padding: 1.3em 0 .7em 1em;
|
||||||
@@ -294,6 +182,8 @@ blink {
|
|||||||
color: #444;
|
color: #444;
|
||||||
background: none;
|
background: none;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
|
margin: 0 .1em;
|
||||||
|
padding: 0 .3em;
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
#mh a:hover {
|
#mh a:hover {
|
||||||
@@ -342,55 +232,6 @@ blink {
|
|||||||
html.dark #toc li {
|
html.dark #toc li {
|
||||||
border-width: 0;
|
border-width: 0;
|
||||||
}
|
}
|
||||||
html.dark #mp a {
|
|
||||||
background: #057;
|
|
||||||
}
|
|
||||||
html.dark #mp h1 a, html.dark #mp h4 a,
|
|
||||||
html.dark #mp h2 a, html.dark #mp h5 a,
|
|
||||||
html.dark #mp h3 a, html.dark #mp h6 a {
|
|
||||||
color: inherit;
|
|
||||||
background: none;
|
|
||||||
}
|
|
||||||
html.dark pre,
|
|
||||||
html.dark code {
|
|
||||||
color: #8c0;
|
|
||||||
background: #1a1a1a;
|
|
||||||
border: .07em solid #333;
|
|
||||||
}
|
|
||||||
html.dark #mp ul,
|
|
||||||
html.dark #mp ol {
|
|
||||||
border-color: #444;
|
|
||||||
}
|
|
||||||
html.dark #m>ul,
|
|
||||||
html.dark #m>ol {
|
|
||||||
border-color: #555;
|
|
||||||
}
|
|
||||||
html.dark strong {
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
html.dark p>em,
|
|
||||||
html.dark li>em,
|
|
||||||
html.dark td>em {
|
|
||||||
color: #f94;
|
|
||||||
border-color: #666;
|
|
||||||
}
|
|
||||||
html.dark h1 {
|
|
||||||
background: #383838;
|
|
||||||
border-top: .4em solid #b80;
|
|
||||||
border-bottom: .4em solid #4c4c4c;
|
|
||||||
}
|
|
||||||
html.dark h2 {
|
|
||||||
background: #444;
|
|
||||||
border-bottom: .22em solid #555;
|
|
||||||
}
|
|
||||||
html.dark td,
|
|
||||||
html.dark th {
|
|
||||||
border-color: #444;
|
|
||||||
}
|
|
||||||
html.dark blockquote {
|
|
||||||
background: #282828;
|
|
||||||
border: .07em dashed #444;
|
|
||||||
}
|
|
||||||
html.dark #mn a:not(:last-child)::after {
|
html.dark #mn a:not(:last-child)::after {
|
||||||
border-color: rgba(255,255,255,0.3);
|
border-color: rgba(255,255,255,0.3);
|
||||||
}
|
}
|
||||||
@@ -496,12 +337,15 @@ blink {
|
|||||||
mso-footer-margin: .6in;
|
mso-footer-margin: .6in;
|
||||||
mso-paper-source: 0;
|
mso-paper-source: 0;
|
||||||
}
|
}
|
||||||
a {
|
.mdo a {
|
||||||
color: #079;
|
color: #079;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
border-bottom: .07em solid #4ac;
|
border-bottom: .07em solid #4ac;
|
||||||
padding: 0 .3em;
|
padding: 0 .3em;
|
||||||
}
|
}
|
||||||
|
#repl {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
#toc>ul {
|
#toc>ul {
|
||||||
border-left: .1em solid #84c4dd;
|
border-left: .1em solid #84c4dd;
|
||||||
}
|
}
|
||||||
@@ -526,18 +370,20 @@ blink {
|
|||||||
a[ctr]::before {
|
a[ctr]::before {
|
||||||
content: attr(ctr) '. ';
|
content: attr(ctr) '. ';
|
||||||
}
|
}
|
||||||
h1 {
|
.mdo h1 {
|
||||||
margin: 2em 0;
|
margin: 2em 0;
|
||||||
}
|
}
|
||||||
h2 {
|
.mdo h2 {
|
||||||
margin: 2em 0 0 0;
|
margin: 2em 0 0 0;
|
||||||
}
|
}
|
||||||
h1, h2, h3 {
|
.mdo h1,
|
||||||
|
.mdo h2,
|
||||||
|
.mdo h3 {
|
||||||
page-break-inside: avoid;
|
page-break-inside: avoid;
|
||||||
}
|
}
|
||||||
h1::after,
|
.mdo h1::after,
|
||||||
h2::after,
|
.mdo h2::after,
|
||||||
h3::after {
|
.mdo h3::after {
|
||||||
content: 'orz';
|
content: 'orz';
|
||||||
color: transparent;
|
color: transparent;
|
||||||
display: block;
|
display: block;
|
||||||
@@ -545,20 +391,20 @@ blink {
|
|||||||
padding: 4em 0 0 0;
|
padding: 4em 0 0 0;
|
||||||
margin: 0 0 -5em 0;
|
margin: 0 0 -5em 0;
|
||||||
}
|
}
|
||||||
p {
|
.mdo p {
|
||||||
page-break-inside: avoid;
|
page-break-inside: avoid;
|
||||||
}
|
}
|
||||||
table {
|
.mdo table {
|
||||||
page-break-inside: auto;
|
page-break-inside: auto;
|
||||||
}
|
}
|
||||||
tr {
|
.mdo tr {
|
||||||
page-break-inside: avoid;
|
page-break-inside: avoid;
|
||||||
page-break-after: auto;
|
page-break-after: auto;
|
||||||
}
|
}
|
||||||
thead {
|
.mdo thead {
|
||||||
display: table-header-group;
|
display: table-header-group;
|
||||||
}
|
}
|
||||||
tfoot {
|
.mdo tfoot {
|
||||||
display: table-footer-group;
|
display: table-footer-group;
|
||||||
}
|
}
|
||||||
#mp a.vis::after {
|
#mp a.vis::after {
|
||||||
@@ -566,31 +412,32 @@ blink {
|
|||||||
border-bottom: 1px solid #bbb;
|
border-bottom: 1px solid #bbb;
|
||||||
color: #444;
|
color: #444;
|
||||||
}
|
}
|
||||||
blockquote {
|
.mdo blockquote {
|
||||||
border-color: #555;
|
border-color: #555;
|
||||||
}
|
}
|
||||||
code {
|
.mdo code {
|
||||||
border-color: #bbb;
|
border-color: #bbb;
|
||||||
}
|
}
|
||||||
pre, pre code {
|
.mdo pre,
|
||||||
|
.mdo pre code {
|
||||||
border-color: #999;
|
border-color: #999;
|
||||||
}
|
}
|
||||||
pre code::before {
|
.mdo pre code::before {
|
||||||
color: #058;
|
color: #058;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
html.dark a {
|
html.dark .mdo a {
|
||||||
color: #000;
|
color: #000;
|
||||||
}
|
}
|
||||||
html.dark pre,
|
html.dark .mdo pre,
|
||||||
html.dark code {
|
html.dark .mdo code {
|
||||||
color: #240;
|
color: #240;
|
||||||
}
|
}
|
||||||
html.dark p>em,
|
html.dark .mdo p>em,
|
||||||
html.dark li>em,
|
html.dark .mdo li>em,
|
||||||
html.dark td>em {
|
html.dark .mdo td>em {
|
||||||
color: #940;
|
color: #940;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
<!DOCTYPE html><html><head>
|
<!DOCTYPE html><html><head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>📝🎉 {{ title }}</title> <!-- 📜 -->
|
<title>📝🎉 {{ title }}</title>
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=0.7">
|
<meta name="viewport" content="width=device-width, initial-scale=0.7">
|
||||||
<link href="/.cpr/md.css?_={{ ts }}" rel="stylesheet">
|
<link rel="stylesheet" href="/.cpr/ui.css?_={{ ts }}">
|
||||||
|
<link rel="stylesheet" href="/.cpr/md.css?_={{ ts }}">
|
||||||
{%- if edit %}
|
{%- if edit %}
|
||||||
<link href="/.cpr/md2.css?_={{ ts }}" rel="stylesheet">
|
<link rel="stylesheet" href="/.cpr/md2.css?_={{ ts }}">
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -14,9 +15,9 @@
|
|||||||
<a id="lightswitch" href="#">go dark</a>
|
<a id="lightswitch" href="#">go dark</a>
|
||||||
<a id="navtoggle" href="#">hide nav</a>
|
<a id="navtoggle" href="#">hide nav</a>
|
||||||
{%- if edit %}
|
{%- if edit %}
|
||||||
<a id="save" href="?edit">save</a>
|
<a id="save" href="?edit" tt="Hotkey: ctrl-s">save</a>
|
||||||
<a id="sbs" href="#">sbs</a>
|
<a id="sbs" href="#" tt="editor and preview side by side">sbs</a>
|
||||||
<a id="nsbs" href="#">editor</a>
|
<a id="nsbs" href="#" tt="switch between editor and preview$NHotkey: ctrl-e">editor</a>
|
||||||
<div id="toolsbox">
|
<div id="toolsbox">
|
||||||
<a id="tools" href="#">tools</a>
|
<a id="tools" href="#">tools</a>
|
||||||
<a id="fmt_table" href="#">prettify table (ctrl-k)</a>
|
<a id="fmt_table" href="#">prettify table (ctrl-k)</a>
|
||||||
@@ -26,8 +27,8 @@
|
|||||||
<a id="help" href="#">help</a>
|
<a id="help" href="#">help</a>
|
||||||
</div>
|
</div>
|
||||||
{%- else %}
|
{%- else %}
|
||||||
<a href="?edit">edit (basic)</a>
|
<a href="?edit" tt="good: higher performance$Ngood: same document width as viewer$Nbad: assumes you know markdown">edit (basic)</a>
|
||||||
<a href="?edit2">edit (fancy)</a>
|
<a href="?edit2" tt="not in-house so probably less buggy">edit (fancy)</a>
|
||||||
<a href="?raw">view raw</a>
|
<a href="?raw">view raw</a>
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
</div>
|
</div>
|
||||||
@@ -42,8 +43,9 @@
|
|||||||
if you're still reading this, check that javascript is allowed
|
if you're still reading this, check that javascript is allowed
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="mp"></div>
|
<div id="mp" class="mdo"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<a href="#" id="repl">π</a>
|
||||||
|
|
||||||
{%- if edit %}
|
{%- if edit %}
|
||||||
<div id="helpbox">
|
<div id="helpbox">
|
||||||
@@ -131,18 +133,18 @@ var md_opt = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
(function () {
|
(function () {
|
||||||
var btn = document.getElementById("lightswitch");
|
var l = localStorage,
|
||||||
var toggle = function (e) {
|
drk = l.getItem('lightmode') != 1,
|
||||||
if (e) e.preventDefault();
|
btn = document.getElementById("lightswitch"),
|
||||||
var dark = !document.documentElement.getAttribute("class");
|
f = function (e) {
|
||||||
document.documentElement.setAttribute("class", dark ? "dark" : "");
|
if (e) { e.preventDefault(); drk = !drk; }
|
||||||
btn.innerHTML = "go " + (dark ? "light" : "dark");
|
document.documentElement.setAttribute("class", drk? "dark":"light");
|
||||||
if (window.localStorage)
|
btn.innerHTML = "go " + (drk ? "light":"dark");
|
||||||
localStorage.setItem('lightmode', dark ? 0 : 1);
|
l.setItem('lightmode', drk? 0:1);
|
||||||
};
|
};
|
||||||
btn.onclick = toggle;
|
|
||||||
if (window.localStorage && localStorage.getItem('lightmode') != 1)
|
btn.onclick = f;
|
||||||
toggle();
|
f();
|
||||||
})();
|
})();
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -24,23 +24,6 @@ var dbg = function () { };
|
|||||||
var md_plug = {};
|
var md_plug = {};
|
||||||
|
|
||||||
|
|
||||||
function hesc(txt) {
|
|
||||||
return txt.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function cls(dom, name, add) {
|
|
||||||
var re = new RegExp('(^| )' + name + '( |$)');
|
|
||||||
var lst = (dom.getAttribute('class') + '').replace(re, "$1$2").replace(/ /, "");
|
|
||||||
dom.setAttribute('class', lst + (add ? ' ' + name : ''));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function statify(obj) {
|
|
||||||
return JSON.parse(JSON.stringify(obj));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// dodge browser issues
|
// dodge browser issues
|
||||||
(function () {
|
(function () {
|
||||||
var ua = navigator.userAgent;
|
var ua = navigator.userAgent;
|
||||||
@@ -65,7 +48,7 @@ function statify(obj) {
|
|||||||
if (a > 0)
|
if (a > 0)
|
||||||
loc.push(n[a]);
|
loc.push(n[a]);
|
||||||
|
|
||||||
var dec = hesc(uricom_dec(n[a])[0]);
|
var dec = esc(uricom_dec(n[a])[0]);
|
||||||
|
|
||||||
nav.push('<a href="/' + loc.join('/') + '">' + dec + '</a>');
|
nav.push('<a href="/' + loc.join('/') + '">' + dec + '</a>');
|
||||||
}
|
}
|
||||||
@@ -73,6 +56,26 @@ function statify(obj) {
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
||||||
|
// image load handler
|
||||||
|
var img_load = (function () {
|
||||||
|
var r = {};
|
||||||
|
r.callbacks = [];
|
||||||
|
|
||||||
|
function fire() {
|
||||||
|
for (var a = 0; a < r.callbacks.length; a++)
|
||||||
|
r.callbacks[a]();
|
||||||
|
}
|
||||||
|
|
||||||
|
var timeout = null;
|
||||||
|
r.done = function () {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(fire, 500);
|
||||||
|
};
|
||||||
|
|
||||||
|
return r;
|
||||||
|
})();
|
||||||
|
|
||||||
|
|
||||||
// faster than replacing the entire html (chrome 1.8x, firefox 1.6x)
|
// faster than replacing the entire html (chrome 1.8x, firefox 1.6x)
|
||||||
function copydom(src, dst, lv) {
|
function copydom(src, dst, lv) {
|
||||||
var sc = src.childNodes,
|
var sc = src.childNodes,
|
||||||
@@ -176,7 +179,7 @@ function md_plug_err(ex, js) {
|
|||||||
var lns = js.split('\n');
|
var lns = js.split('\n');
|
||||||
if (ln < lns.length) {
|
if (ln < lns.length) {
|
||||||
o = mknod('span');
|
o = mknod('span');
|
||||||
o.style.cssText = 'color:#ac2;font-size:.9em;font-family:scp;display:block';
|
o.style.cssText = "color:#ac2;font-size:.9em;font-family:'scp',monospace,monospace;display:block";
|
||||||
o.textContent = lns[ln - 1];
|
o.textContent = lns[ln - 1];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -185,7 +188,7 @@ function md_plug_err(ex, js) {
|
|||||||
errbox.style.cssText = 'position:absolute;top:0;left:0;padding:1em .5em;background:#2b2b2b;color:#fc5'
|
errbox.style.cssText = 'position:absolute;top:0;left:0;padding:1em .5em;background:#2b2b2b;color:#fc5'
|
||||||
errbox.textContent = msg;
|
errbox.textContent = msg;
|
||||||
errbox.onclick = function () {
|
errbox.onclick = function () {
|
||||||
alert('' + ex.stack);
|
modal.alert('<pre>' + esc(ex.stack) + '</pre>');
|
||||||
};
|
};
|
||||||
if (o) {
|
if (o) {
|
||||||
errbox.appendChild(o);
|
errbox.appendChild(o);
|
||||||
@@ -356,6 +359,10 @@ function convert_markdown(md_text, dest_dom) {
|
|||||||
|
|
||||||
copydom(md_dom, dest_dom, 0);
|
copydom(md_dom, dest_dom, 0);
|
||||||
|
|
||||||
|
var imgs = dest_dom.getElementsByTagName('img');
|
||||||
|
for (var a = 0, aa = imgs.length; a < aa; a++)
|
||||||
|
imgs[a].onload = img_load.done;
|
||||||
|
|
||||||
if (ext && ext[0].render2)
|
if (ext && ext[0].render2)
|
||||||
try {
|
try {
|
||||||
ext[0].render2(dest_dom);
|
ext[0].render2(dest_dom);
|
||||||
@@ -490,6 +497,7 @@ function init_toc() {
|
|||||||
// "main" :p
|
// "main" :p
|
||||||
convert_markdown(dom_src.value, dom_pre);
|
convert_markdown(dom_src.value, dom_pre);
|
||||||
var toc = init_toc();
|
var toc = init_toc();
|
||||||
|
img_load.callbacks = [toc.refresh];
|
||||||
|
|
||||||
|
|
||||||
// scroll handler
|
// scroll handler
|
||||||
@@ -530,3 +538,6 @@ dom_navtgl.onclick = function () {
|
|||||||
|
|
||||||
if (sread('hidenav') == 1)
|
if (sread('hidenav') == 1)
|
||||||
dom_navtgl.onclick();
|
dom_navtgl.onclick();
|
||||||
|
|
||||||
|
if (window['tt'])
|
||||||
|
tt.init();
|
||||||
|
|||||||
@@ -50,7 +50,7 @@
|
|||||||
outline: none;
|
outline: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: 'consolas', monospace, monospace;
|
font-family: 'scp', monospace, monospace;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
@@ -84,13 +84,10 @@ html.dark #save.force-save {
|
|||||||
#save.disabled {
|
#save.disabled {
|
||||||
opacity: .4;
|
opacity: .4;
|
||||||
}
|
}
|
||||||
#helpbox,
|
#helpbox {
|
||||||
#toast {
|
|
||||||
background: #f7f7f7;
|
background: #f7f7f7;
|
||||||
border-radius: .4em;
|
border-radius: .4em;
|
||||||
z-index: 9001;
|
z-index: 9001;
|
||||||
}
|
|
||||||
#helpbox {
|
|
||||||
display: none;
|
display: none;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
padding: 2em;
|
padding: 2em;
|
||||||
@@ -107,19 +104,7 @@ html.dark #save.force-save {
|
|||||||
}
|
}
|
||||||
html.dark #helpbox {
|
html.dark #helpbox {
|
||||||
box-shadow: 0 .5em 2em #444;
|
box-shadow: 0 .5em 2em #444;
|
||||||
}
|
|
||||||
html.dark #helpbox,
|
|
||||||
html.dark #toast {
|
|
||||||
background: #222;
|
background: #222;
|
||||||
border: 1px solid #079;
|
border: 1px solid #079;
|
||||||
border-width: 1px 0;
|
border-width: 1px 0;
|
||||||
}
|
}
|
||||||
#toast {
|
|
||||||
font-weight: bold;
|
|
||||||
text-align: center;
|
|
||||||
padding: .6em 0;
|
|
||||||
position: fixed;
|
|
||||||
top: 30%;
|
|
||||||
transition: opacity 0.2s ease-in-out;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ var draw_md = (function () {
|
|||||||
var src = dom_src.value;
|
var src = dom_src.value;
|
||||||
convert_markdown(src, dom_pre);
|
convert_markdown(src, dom_pre);
|
||||||
|
|
||||||
var lines = hesc(src).replace(/\r/g, "").split('\n');
|
var lines = esc(src).replace(/\r/g, "").split('\n');
|
||||||
nlines = lines.length;
|
nlines = lines.length;
|
||||||
var html = [];
|
var html = [];
|
||||||
for (var a = 0; a < lines.length; a++)
|
for (var a = 0; a < lines.length; a++)
|
||||||
@@ -108,7 +108,7 @@ var draw_md = (function () {
|
|||||||
map_src = genmap(dom_ref, map_src);
|
map_src = genmap(dom_ref, map_src);
|
||||||
map_pre = genmap(dom_pre, map_pre);
|
map_pre = genmap(dom_pre, map_pre);
|
||||||
|
|
||||||
cls(ebi('save'), 'disabled', src == server_md);
|
clmod(ebi('save'), 'disabled', src == server_md);
|
||||||
|
|
||||||
var t1 = Date.now();
|
var t1 = Date.now();
|
||||||
delay = t1 - t0 > 100 ? 25 : 1;
|
delay = t1 - t0 > 100 ? 25 : 1;
|
||||||
@@ -127,6 +127,12 @@ var draw_md = (function () {
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
||||||
|
// discard TOC callback, just regen editor scroll map
|
||||||
|
img_load.callbacks = [function () {
|
||||||
|
map_pre = genmap(dom_pre, map_pre);
|
||||||
|
}];
|
||||||
|
|
||||||
|
|
||||||
// resize handler
|
// resize handler
|
||||||
redraw = (function () {
|
redraw = (function () {
|
||||||
function onresize() {
|
function onresize() {
|
||||||
@@ -136,7 +142,6 @@ redraw = (function () {
|
|||||||
dom_ref.style.width = getComputedStyle(dom_src).offsetWidth + 'px';
|
dom_ref.style.width = getComputedStyle(dom_src).offsetWidth + 'px';
|
||||||
map_src = genmap(dom_ref, map_src);
|
map_src = genmap(dom_ref, map_src);
|
||||||
map_pre = genmap(dom_pre, map_pre);
|
map_pre = genmap(dom_pre, map_pre);
|
||||||
dbg(document.body.clientWidth + 'x' + document.body.clientHeight);
|
|
||||||
}
|
}
|
||||||
function setsbs() {
|
function setsbs() {
|
||||||
dom_wrap.setAttribute('class', '');
|
dom_wrap.setAttribute('class', '');
|
||||||
@@ -236,7 +241,7 @@ function Modpoll() {
|
|||||||
|
|
||||||
var skip = null;
|
var skip = null;
|
||||||
|
|
||||||
if (ebi('toast'))
|
if (toast.visible)
|
||||||
skip = 'toast';
|
skip = 'toast';
|
||||||
|
|
||||||
else if (this.skip_one)
|
else if (this.skip_one)
|
||||||
@@ -285,16 +290,15 @@ function Modpoll() {
|
|||||||
console.log("modpoll diff |" + server_ref.length + "|, |" + server_now.length + "|");
|
console.log("modpoll diff |" + server_ref.length + "|, |" + server_now.length + "|");
|
||||||
this.modpoll.disabled = true;
|
this.modpoll.disabled = true;
|
||||||
var msg = [
|
var msg = [
|
||||||
"The document has changed on the server.<br />" +
|
"The document has changed on the server.",
|
||||||
"The changes will NOT be loaded into your editor automatically.",
|
"The changes will NOT be loaded into your editor automatically.",
|
||||||
|
"",
|
||||||
"Press F5 or CTRL-R to refresh the page,<br />" +
|
"Press F5 or CTRL-R to refresh the page,",
|
||||||
"replacing your document with the server copy.",
|
"replacing your document with the server copy.",
|
||||||
|
"",
|
||||||
"You can click this message to ignore and contnue."
|
"You can close this message to ignore and contnue."
|
||||||
];
|
];
|
||||||
return toast(false, "box-shadow:0 1em 2em rgba(64,64,64,0.8);font-weight:normal",
|
return toast.warn(0, msg.join('\n'));
|
||||||
36, "<p>" + msg.join('</p>\n<p>') + '</p>');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('modpoll eq');
|
console.log('modpoll eq');
|
||||||
@@ -323,20 +327,14 @@ function save(e) {
|
|||||||
var save_btn = ebi("save"),
|
var save_btn = ebi("save"),
|
||||||
save_cls = save_btn.getAttribute('class') + '';
|
save_cls = save_btn.getAttribute('class') + '';
|
||||||
|
|
||||||
if (save_cls.indexOf('disabled') >= 0) {
|
if (save_cls.indexOf('disabled') >= 0)
|
||||||
toast(true, ";font-size:2em;color:#c90", 9, "no changes");
|
return toast.inf(2, "no changes");
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var force = (save_cls.indexOf('force-save') >= 0);
|
var force = (save_cls.indexOf('force-save') >= 0);
|
||||||
if (force && !confirm('confirm that you wish to lose the changes made on the server since you opened this document')) {
|
function save2() {
|
||||||
alert('ok, aborted');
|
var txt = dom_src.value,
|
||||||
return;
|
fd = new FormData();
|
||||||
}
|
|
||||||
|
|
||||||
var txt = dom_src.value;
|
|
||||||
|
|
||||||
var fd = new FormData();
|
|
||||||
fd.append("act", "tput");
|
fd.append("act", "tput");
|
||||||
fd.append("lastmod", (force ? -1 : last_modified));
|
fd.append("lastmod", (force ? -1 : last_modified));
|
||||||
fd.append("body", txt);
|
fd.append("body", txt);
|
||||||
@@ -353,27 +351,32 @@ function save(e) {
|
|||||||
xhr.send(fd);
|
xhr.send(fd);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!force)
|
||||||
|
save2();
|
||||||
|
else
|
||||||
|
modal.confirm('confirm that you wish to lose the changes made on the server since you opened this document', save2, function () {
|
||||||
|
toast.inf(3, 'aborted');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function save_cb() {
|
function save_cb() {
|
||||||
if (this.readyState != XMLHttpRequest.DONE)
|
if (this.readyState != XMLHttpRequest.DONE)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (this.status !== 200) {
|
if (this.status !== 200)
|
||||||
alert('Error! The file was NOT saved.\n\n' + this.status + ": " + (this.responseText + '').replace(/^<pre>/, ""));
|
return toast.err(0, 'Error! The file was NOT saved.\n\n' + this.status + ": " + (this.responseText + '').replace(/^<pre>/, ""));
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var r;
|
var r;
|
||||||
try {
|
try {
|
||||||
r = JSON.parse(this.responseText);
|
r = JSON.parse(this.responseText);
|
||||||
}
|
}
|
||||||
catch (ex) {
|
catch (ex) {
|
||||||
alert('Failed to parse reply from server:\n\n' + this.responseText);
|
return toast.err(0, 'Failed to parse reply from server:\n\n' + this.responseText);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!r.ok) {
|
if (!r.ok) {
|
||||||
if (!this.btn.classList.contains('force-save')) {
|
if (!clgot(this.btn, 'force-save')) {
|
||||||
this.btn.classList.add('force-save');
|
clmod(this.btn, 'force-save', 1);
|
||||||
var msg = [
|
var msg = [
|
||||||
'This file has been modified since you started editing it!\n',
|
'This file has been modified since you started editing it!\n',
|
||||||
'if you really want to overwrite, press save again.\n',
|
'if you really want to overwrite, press save again.\n',
|
||||||
@@ -383,15 +386,13 @@ function save_cb() {
|
|||||||
r.lastmod + ' lastmod on the server now,',
|
r.lastmod + ' lastmod on the server now,',
|
||||||
r.now + ' server time now,\n',
|
r.now + ' server time now,\n',
|
||||||
];
|
];
|
||||||
alert(msg.join('\n'));
|
return toast.err(0, msg.join('\n'));
|
||||||
}
|
}
|
||||||
else {
|
else
|
||||||
alert('Error! Save failed. Maybe this JSON explains why:\n\n' + this.responseText);
|
return toast.err(0, 'Error! Save failed. Maybe this JSON explains why:\n\n' + this.responseText);
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.btn.classList.remove('force-save');
|
clmod(this.btn, 'force-save');
|
||||||
//alert('save OK -- wrote ' + r.size + ' bytes.\n\nsha512: ' + r.sha512);
|
//alert('save OK -- wrote ' + r.size + ' bytes.\n\nsha512: ' + r.sha512);
|
||||||
|
|
||||||
run_savechk(r.lastmod, this.txt, this.btn, 0);
|
run_savechk(r.lastmod, this.txt, this.btn, 0);
|
||||||
@@ -415,10 +416,8 @@ function savechk_cb() {
|
|||||||
if (this.readyState != XMLHttpRequest.DONE)
|
if (this.readyState != XMLHttpRequest.DONE)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (this.status !== 200) {
|
if (this.status !== 200)
|
||||||
alert('Error! The file was NOT saved.\n\n' + this.status + ": " + (this.responseText + '').replace(/^<pre>/, ""));
|
return toast.err(0, 'Error! The file was NOT saved.\n\n' + this.status + ": " + (this.responseText + '').replace(/^<pre>/, ""));
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var doc1 = this.txt.replace(/\r\n/g, "\n");
|
var doc1 = this.txt.replace(/\r\n/g, "\n");
|
||||||
var doc2 = this.responseText.replace(/\r\n/g, "\n");
|
var doc2 = this.responseText.replace(/\r\n/g, "\n");
|
||||||
@@ -431,58 +430,22 @@ function savechk_cb() {
|
|||||||
}, 100);
|
}, 100);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
alert(
|
modal.alert(
|
||||||
'Error! The document on the server does not appear to have saved correctly (your editor contents and the server copy is not identical). Place the document on your clipboard for now and check the server logs for hints\n\n' +
|
'Error! The document on the server does not appear to have saved correctly (your editor contents and the server copy is not identical). Place the document on your clipboard for now and check the server logs for hints\n\n' +
|
||||||
'Length: yours=' + doc1.length + ', server=' + doc2.length
|
'Length: yours=' + doc1.length + ', server=' + doc2.length
|
||||||
);
|
);
|
||||||
alert('yours, ' + doc1.length + ' byte:\n[' + doc1 + ']');
|
modal.alert('yours, ' + doc1.length + ' byte:\n[' + doc1 + ']');
|
||||||
alert('server, ' + doc2.length + ' byte:\n[' + doc2 + ']');
|
modal.alert('server, ' + doc2.length + ' byte:\n[' + doc2 + ']');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
last_modified = this.lastmod;
|
last_modified = this.lastmod;
|
||||||
server_md = this.txt;
|
server_md = this.txt;
|
||||||
draw_md();
|
draw_md();
|
||||||
toast(true, ";font-size:6em;font-family:serif;color:#9b4", 4,
|
toast.ok(2, 'save OK' + (this.ntry ? '\nattempt ' + this.ntry : ''));
|
||||||
'OK✔️<span style="font-size:.2em;color:#999;position:absolute">' + this.ntry + '</span>');
|
|
||||||
|
|
||||||
modpoll.disabled = false;
|
modpoll.disabled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function toast(autoclose, style, width, msg) {
|
|
||||||
var ok = ebi("toast");
|
|
||||||
if (ok)
|
|
||||||
ok.parentNode.removeChild(ok);
|
|
||||||
|
|
||||||
style = "width:" + width + "em;left:calc(50% - " + (width / 2) + "em);" + style;
|
|
||||||
ok = mknod('div');
|
|
||||||
ok.setAttribute('id', 'toast');
|
|
||||||
ok.setAttribute('style', style);
|
|
||||||
ok.innerHTML = msg;
|
|
||||||
var parent = ebi('m');
|
|
||||||
document.documentElement.appendChild(ok);
|
|
||||||
|
|
||||||
var hide = function (delay) {
|
|
||||||
delay = delay || 0;
|
|
||||||
|
|
||||||
setTimeout(function () {
|
|
||||||
ok.style.opacity = 0;
|
|
||||||
}, delay);
|
|
||||||
|
|
||||||
setTimeout(function () {
|
|
||||||
if (ok.parentNode)
|
|
||||||
ok.parentNode.removeChild(ok);
|
|
||||||
}, delay + 250);
|
|
||||||
}
|
|
||||||
|
|
||||||
ok.onclick = function () {
|
|
||||||
hide(0);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (autoclose)
|
|
||||||
hide(500);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// firefox bug: initial selection offset isn't cleared properly through js
|
// firefox bug: initial selection offset isn't cleared properly through js
|
||||||
var ff_clearsel = (function () {
|
var ff_clearsel = (function () {
|
||||||
@@ -761,7 +724,7 @@ function fmt_table(e) {
|
|||||||
|
|
||||||
var ind2 = tab[a].match(re_ind)[0];
|
var ind2 = tab[a].match(re_ind)[0];
|
||||||
if (ind != ind2 && a != 1) // the table can be a list entry or something, ignore [0]
|
if (ind != ind2 && a != 1) // the table can be a list entry or something, ignore [0]
|
||||||
return alert(err + 'indentation mismatch on row#2 and ' + row_name + ',\n' + tab[a]);
|
return toast.err(7, err + 'indentation mismatch on row#2 and ' + row_name + ',\n' + tab[a]);
|
||||||
|
|
||||||
var t = tab[a].slice(ind.length);
|
var t = tab[a].slice(ind.length);
|
||||||
t = t.replace(re_lpipe, "");
|
t = t.replace(re_lpipe, "");
|
||||||
@@ -771,7 +734,7 @@ function fmt_table(e) {
|
|||||||
if (a == 0)
|
if (a == 0)
|
||||||
ncols = tab[a].length;
|
ncols = tab[a].length;
|
||||||
else if (ncols < tab[a].length)
|
else if (ncols < tab[a].length)
|
||||||
return alert(err + 'num.columns(' + row_name + ') exceeding row#2; ' + ncols + ' < ' + tab[a].length);
|
return toast.err(7, err + 'num.columns(' + row_name + ') exceeding row#2; ' + ncols + ' < ' + tab[a].length);
|
||||||
|
|
||||||
// if row has less columns than row2, fill them in
|
// if row has less columns than row2, fill them in
|
||||||
while (tab[a].length < ncols)
|
while (tab[a].length < ncols)
|
||||||
@@ -788,7 +751,7 @@ function fmt_table(e) {
|
|||||||
for (var col = 0; col < tab[1].length; col++) {
|
for (var col = 0; col < tab[1].length; col++) {
|
||||||
var m = tab[1][col].match(re_align);
|
var m = tab[1][col].match(re_align);
|
||||||
if (!m)
|
if (!m)
|
||||||
return alert(err + 'invalid column specification, row#2, col ' + (col + 1) + ', [' + tab[1][col] + ']');
|
return toast.err(7, err + 'invalid column specification, row#2, col ' + (col + 1) + ', [' + tab[1][col] + ']');
|
||||||
|
|
||||||
if (m[2]) {
|
if (m[2]) {
|
||||||
if (m[1])
|
if (m[1])
|
||||||
@@ -876,10 +839,9 @@ function mark_uni(e) {
|
|||||||
ptn = new RegExp('([^' + js_uni_whitelist + ']+)', 'g'),
|
ptn = new RegExp('([^' + js_uni_whitelist + ']+)', 'g'),
|
||||||
mod = txt.replace(/\r/g, "").replace(ptn, "\u2588\u2770$1\u2771");
|
mod = txt.replace(/\r/g, "").replace(ptn, "\u2588\u2770$1\u2771");
|
||||||
|
|
||||||
if (txt == mod) {
|
if (txt == mod)
|
||||||
alert('no results; no modifications were made');
|
return toast.inf(5, 'no results; no modifications were made');
|
||||||
return;
|
|
||||||
}
|
|
||||||
dom_src.value = mod;
|
dom_src.value = mod;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -893,10 +855,9 @@ function iter_uni(e) {
|
|||||||
re = new RegExp('([^' + js_uni_whitelist + ']+)'),
|
re = new RegExp('([^' + js_uni_whitelist + ']+)'),
|
||||||
m = re.exec(txt.slice(ofs));
|
m = re.exec(txt.slice(ofs));
|
||||||
|
|
||||||
if (!m) {
|
if (!m)
|
||||||
alert('no more hits from cursor onwards');
|
return toast.inf(5, 'no more hits from cursor onwards');
|
||||||
return;
|
|
||||||
}
|
|
||||||
ofs += m.index;
|
ofs += m.index;
|
||||||
|
|
||||||
dom_src.setSelectionRange(ofs, ofs + m[0].length, "forward");
|
dom_src.setSelectionRange(ofs, ofs + m[0].length, "forward");
|
||||||
@@ -911,12 +872,10 @@ function iter_uni(e) {
|
|||||||
function cfg_uni(e) {
|
function cfg_uni(e) {
|
||||||
if (e) e.preventDefault();
|
if (e) e.preventDefault();
|
||||||
|
|
||||||
var reply = prompt("unicode whitelist", esc_uni_whitelist);
|
modal.prompt("unicode whitelist", esc_uni_whitelist, function (reply) {
|
||||||
if (reply === null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
esc_uni_whitelist = reply;
|
esc_uni_whitelist = reply;
|
||||||
js_uni_whitelist = eval('\'' + esc_uni_whitelist + '\'');
|
js_uni_whitelist = eval('\'' + esc_uni_whitelist + '\'');
|
||||||
|
}, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -924,10 +883,9 @@ function cfg_uni(e) {
|
|||||||
(function () {
|
(function () {
|
||||||
function keydown(ev) {
|
function keydown(ev) {
|
||||||
ev = ev || window.event;
|
ev = ev || window.event;
|
||||||
var kc = ev.keyCode || ev.which;
|
var kc = ev.code || ev.keyCode || ev.which;
|
||||||
var ctrl = ev.ctrlKey || ev.metaKey;
|
//console.log(ev.key, ev.code, ev.keyCode, ev.which);
|
||||||
//console.log(ev.code, kc);
|
if (ctrl(ev) && (ev.code == "KeyS" || kc == 83)) {
|
||||||
if (ctrl && (ev.code == "KeyS" || kc == 83)) {
|
|
||||||
save();
|
save();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -936,23 +894,15 @@ function cfg_uni(e) {
|
|||||||
if (d)
|
if (d)
|
||||||
d.click();
|
d.click();
|
||||||
}
|
}
|
||||||
if (document.activeElement == dom_src) {
|
if (document.activeElement != dom_src)
|
||||||
if (ev.code == "Tab" || kc == 9) {
|
return true;
|
||||||
md_indent(ev.shiftKey);
|
|
||||||
return false;
|
if (ctrl(ev)) {
|
||||||
}
|
if (ev.code == "KeyH" || kc == 72) {
|
||||||
if (ctrl && (ev.code == "KeyH" || kc == 72)) {
|
|
||||||
md_header(ev.shiftKey);
|
md_header(ev.shiftKey);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!ctrl && (ev.code == "Home" || kc == 36)) {
|
if (ev.code == "KeyZ" || kc == 90) {
|
||||||
md_home(ev.shiftKey);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!ctrl && !ev.shiftKey && (ev.code == "Enter" || kc == 13)) {
|
|
||||||
return md_newline();
|
|
||||||
}
|
|
||||||
if (ctrl && (ev.code == "KeyZ" || kc == 90)) {
|
|
||||||
if (ev.shiftKey)
|
if (ev.shiftKey)
|
||||||
action_stack.redo();
|
action_stack.redo();
|
||||||
else
|
else
|
||||||
@@ -960,33 +910,45 @@ function cfg_uni(e) {
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (ctrl && (ev.code == "KeyY" || kc == 89)) {
|
if (ev.code == "KeyY" || kc == 89) {
|
||||||
action_stack.redo();
|
action_stack.redo();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!ctrl && !ev.shiftKey && kc == 8) {
|
if (ev.code == "KeyK") {
|
||||||
return md_backspace();
|
|
||||||
}
|
|
||||||
if (ctrl && (ev.code == "KeyK")) {
|
|
||||||
fmt_table();
|
fmt_table();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (ctrl && (ev.code == "KeyU")) {
|
if (ev.code == "KeyU") {
|
||||||
iter_uni();
|
iter_uni();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (ctrl && (ev.code == "KeyE")) {
|
if (ev.code == "KeyE") {
|
||||||
dom_nsbs.click();
|
dom_nsbs.click();
|
||||||
//fmt_table();
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
var up = ev.code == "ArrowUp" || kc == 38;
|
var up = ev.code == "ArrowUp" || kc == 38;
|
||||||
var dn = ev.code == "ArrowDown" || kc == 40;
|
var dn = ev.code == "ArrowDown" || kc == 40;
|
||||||
if (ctrl && (up || dn)) {
|
if (up || dn) {
|
||||||
md_p_jump(dn);
|
md_p_jump(dn);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
if (ev.code == "Tab" || kc == 9) {
|
||||||
|
md_indent(ev.shiftKey);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (ev.code == "Home" || kc == 36) {
|
||||||
|
md_home(ev.shiftKey);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!ev.shiftKey && (ev.code == "Enter" || kc == 13)) {
|
||||||
|
return md_newline();
|
||||||
|
}
|
||||||
|
if (!ev.shiftKey && kc == 8) {
|
||||||
|
return md_backspace();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
document.onkeydown = keydown;
|
document.onkeydown = keydown;
|
||||||
ebi('save').onclick = save;
|
ebi('save').onclick = save;
|
||||||
@@ -1129,9 +1091,9 @@ action_stack = (function () {
|
|||||||
ref = newtxt;
|
ref = newtxt;
|
||||||
dbg('undos(%d) redos(%d)', hist.un.length, hist.re.length);
|
dbg('undos(%d) redos(%d)', hist.un.length, hist.re.length);
|
||||||
if (hist.un.length > 0)
|
if (hist.un.length > 0)
|
||||||
dbg(statify(hist.un.slice(-1)[0]));
|
dbg(jcp(hist.un.slice(-1)[0]));
|
||||||
if (hist.re.length > 0)
|
if (hist.re.length > 0)
|
||||||
dbg(statify(hist.re.slice(-1)[0]));
|
dbg(jcp(hist.re.slice(-1)[0]));
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ html .editor-toolbar>button.active { border-color: rgba(0,0,0,0.4); background:
|
|||||||
html .editor-toolbar>i.separator { border-left: 1px solid #ccc; }
|
html .editor-toolbar>i.separator { border-left: 1px solid #ccc; }
|
||||||
html .editor-toolbar.disabled-for-preview>button:not(.no-disable) { opacity: .35 }
|
html .editor-toolbar.disabled-for-preview>button:not(.no-disable) { opacity: .35 }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
html {
|
html {
|
||||||
line-height: 1.5em;
|
line-height: 1.5em;
|
||||||
}
|
}
|
||||||
@@ -18,6 +20,22 @@ html, body {
|
|||||||
background: #f7f7f7;
|
background: #f7f7f7;
|
||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
#toast {
|
||||||
|
bottom: auto;
|
||||||
|
top: 1.4em;
|
||||||
|
}
|
||||||
|
#repl {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: .5em;
|
||||||
|
border: none;
|
||||||
|
color: inherit;
|
||||||
|
background: none;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#mn {
|
#mn {
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
margin: 1.3em 0 .7em 1em;
|
margin: 1.3em 0 .7em 1em;
|
||||||
@@ -59,148 +77,12 @@ html .editor-toolbar>button.disabled {
|
|||||||
html .editor-toolbar>button.save.force-save {
|
html .editor-toolbar>button.save.force-save {
|
||||||
background: #f97;
|
background: #f97;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* copied from md.css for now */
|
|
||||||
.mdo pre,
|
|
||||||
.mdo code,
|
|
||||||
.mdo a {
|
|
||||||
color: #480;
|
|
||||||
background: #f7f7f7;
|
|
||||||
border: .07em solid #ddd;
|
|
||||||
border-radius: .2em;
|
|
||||||
padding: .1em .3em;
|
|
||||||
margin: 0 .1em;
|
|
||||||
}
|
|
||||||
.mdo code {
|
|
||||||
font-size: .96em;
|
|
||||||
}
|
|
||||||
.mdo pre,
|
|
||||||
.mdo code {
|
|
||||||
font-family: monospace, monospace;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
.mdo pre code {
|
|
||||||
display: block;
|
|
||||||
margin: 0 -.3em;
|
|
||||||
padding: .4em .5em;
|
|
||||||
line-height: 1.1em;
|
|
||||||
}
|
|
||||||
.mdo a {
|
|
||||||
color: #fff;
|
|
||||||
background: #39b;
|
|
||||||
text-decoration: none;
|
|
||||||
padding: 0 .3em;
|
|
||||||
border: none;
|
|
||||||
border-bottom: .07em solid #079;
|
|
||||||
}
|
|
||||||
.mdo h2 {
|
|
||||||
color: #fff;
|
|
||||||
background: #555;
|
|
||||||
margin-top: 2em;
|
|
||||||
border-bottom: .22em solid #999;
|
|
||||||
border-top: none;
|
|
||||||
}
|
|
||||||
.mdo h1 {
|
|
||||||
color: #fff;
|
|
||||||
background: #444;
|
|
||||||
font-weight: normal;
|
|
||||||
border-top: .4em solid #fb0;
|
|
||||||
border-bottom: .4em solid #777;
|
|
||||||
border-radius: 0 1em 0 1em;
|
|
||||||
margin: 3em 0 1em 0;
|
|
||||||
padding: .5em 0;
|
|
||||||
}
|
|
||||||
h1, h2 {
|
|
||||||
line-height: 1.5em;
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
font-size: 1.7em;
|
|
||||||
text-align: center;
|
|
||||||
border: 1em solid #777;
|
|
||||||
border-width: .05em 0;
|
|
||||||
margin: 3em 0;
|
|
||||||
}
|
|
||||||
h2 {
|
|
||||||
font-size: 1.5em;
|
|
||||||
font-weight: normal;
|
|
||||||
background: #f7f7f7;
|
|
||||||
border-top: .07em solid #fff;
|
|
||||||
border-bottom: .07em solid #bbb;
|
|
||||||
border-radius: .5em .5em 0 0;
|
|
||||||
padding-left: .4em;
|
|
||||||
margin-top: 3em;
|
|
||||||
}
|
|
||||||
.mdo ul,
|
|
||||||
.mdo ol {
|
|
||||||
border-left: .3em solid #ddd;
|
|
||||||
}
|
|
||||||
.mdo>ul,
|
|
||||||
.mdo>ol {
|
|
||||||
border-color: #bbb;
|
|
||||||
}
|
|
||||||
.mdo ul>li {
|
|
||||||
list-style-type: disc;
|
|
||||||
}
|
|
||||||
.mdo ul>li,
|
|
||||||
.mdo ol>li {
|
|
||||||
margin: .7em 0;
|
|
||||||
}
|
|
||||||
strong {
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
p>em,
|
|
||||||
li>em,
|
|
||||||
td>em {
|
|
||||||
color: #c50;
|
|
||||||
padding: .1em;
|
|
||||||
border-bottom: .1em solid #bbb;
|
|
||||||
}
|
|
||||||
blockquote {
|
|
||||||
font-family: serif;
|
|
||||||
background: #f7f7f7;
|
|
||||||
border: .07em dashed #ccc;
|
|
||||||
padding: 0 2em;
|
|
||||||
margin: 1em 0;
|
|
||||||
}
|
|
||||||
small {
|
|
||||||
opacity: .8;
|
|
||||||
}
|
|
||||||
table {
|
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
|
||||||
td {
|
|
||||||
padding: .2em .5em;
|
|
||||||
border: .12em solid #aaa;
|
|
||||||
}
|
|
||||||
th {
|
|
||||||
border: .12em solid #aaa;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* mde support */
|
|
||||||
.mdo {
|
|
||||||
padding: 1em;
|
|
||||||
background: #f7f7f7;
|
|
||||||
}
|
|
||||||
html.dark .mdo {
|
|
||||||
background: #1c1c1c;
|
|
||||||
}
|
|
||||||
.CodeMirror {
|
.CodeMirror {
|
||||||
background: #f7f7f7;
|
background: #f7f7f7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* darkmode */
|
/* darkmode */
|
||||||
html.dark .mdo,
|
html.dark .mdo,
|
||||||
html.dark .CodeMirror {
|
html.dark .CodeMirror {
|
||||||
@@ -224,55 +106,6 @@ html.dark .CodeMirror-selectedtext {
|
|||||||
background: #246;
|
background: #246;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
html.dark .mdo a {
|
|
||||||
background: #057;
|
|
||||||
}
|
|
||||||
html.dark .mdo h1 a, html.dark .mdo h4 a,
|
|
||||||
html.dark .mdo h2 a, html.dark .mdo h5 a,
|
|
||||||
html.dark .mdo h3 a, html.dark .mdo h6 a {
|
|
||||||
color: inherit;
|
|
||||||
background: none;
|
|
||||||
}
|
|
||||||
html.dark pre,
|
|
||||||
html.dark code {
|
|
||||||
color: #8c0;
|
|
||||||
background: #1a1a1a;
|
|
||||||
border: .07em solid #333;
|
|
||||||
}
|
|
||||||
html.dark .mdo ul,
|
|
||||||
html.dark .mdo ol {
|
|
||||||
border-color: #444;
|
|
||||||
}
|
|
||||||
html.dark .mdo>ul,
|
|
||||||
html.dark .mdo>ol {
|
|
||||||
border-color: #555;
|
|
||||||
}
|
|
||||||
html.dark strong {
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
html.dark p>em,
|
|
||||||
html.dark li>em,
|
|
||||||
html.dark td>em {
|
|
||||||
color: #f94;
|
|
||||||
border-color: #666;
|
|
||||||
}
|
|
||||||
html.dark h1 {
|
|
||||||
background: #383838;
|
|
||||||
border-top: .4em solid #b80;
|
|
||||||
border-bottom: .4em solid #4c4c4c;
|
|
||||||
}
|
|
||||||
html.dark h2 {
|
|
||||||
background: #444;
|
|
||||||
border-bottom: .22em solid #555;
|
|
||||||
}
|
|
||||||
html.dark td,
|
|
||||||
html.dark th {
|
|
||||||
border-color: #444;
|
|
||||||
}
|
|
||||||
html.dark blockquote {
|
|
||||||
background: #282828;
|
|
||||||
border: .07em dashed #444;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -309,3 +142,14 @@ html.dark .editor-toolbar::after,
|
|||||||
html.dark .editor-toolbar::before {
|
html.dark .editor-toolbar::before {
|
||||||
background: none;
|
background: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* ui.css overrides */
|
||||||
|
.mdo {
|
||||||
|
padding: 1em;
|
||||||
|
background: #f7f7f7;
|
||||||
|
}
|
||||||
|
html.dark .mdo {
|
||||||
|
background: #1c1c1c;
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,9 +3,10 @@
|
|||||||
<title>📝🎉 {{ title }}</title>
|
<title>📝🎉 {{ title }}</title>
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=0.7">
|
<meta name="viewport" content="width=device-width, initial-scale=0.7">
|
||||||
<link href="/.cpr/mde.css?_={{ ts }}" rel="stylesheet">
|
<link rel="stylesheet" href="/.cpr/ui.css?_={{ ts }}">
|
||||||
<link href="/.cpr/deps/mini-fa.css?_={{ ts }}" rel="stylesheet">
|
<link rel="stylesheet" href="/.cpr/mde.css?_={{ ts }}">
|
||||||
<link href="/.cpr/deps/easymde.css?_={{ ts }}" rel="stylesheet">
|
<link rel="stylesheet" href="/.cpr/deps/mini-fa.css?_={{ ts }}">
|
||||||
|
<link rel="stylesheet" href="/.cpr/deps/easymde.css?_={{ ts }}">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="mw">
|
<div id="mw">
|
||||||
@@ -20,6 +21,7 @@
|
|||||||
<textarea id="mt" style="display:none" autocomplete="off">{{ md }}</textarea>
|
<textarea id="mt" style="display:none" autocomplete="off">{{ md }}</textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<a href="#" id="repl">π</a>
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
var last_modified = {{ lastmod }};
|
var last_modified = {{ lastmod }};
|
||||||
@@ -30,16 +32,15 @@ var md_opt = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
var lightswitch = (function () {
|
var lightswitch = (function () {
|
||||||
var fun = function () {
|
var l = localStorage,
|
||||||
var dark = !document.documentElement.getAttribute("class");
|
drk = l.getItem('lightmode') != 1,
|
||||||
document.documentElement.setAttribute("class", dark ? "dark" : "");
|
f = function (e) {
|
||||||
if (window.localStorage)
|
if (e) drk = !drk;
|
||||||
localStorage.setItem('lightmode', dark ? 0 : 1);
|
document.documentElement.setAttribute("class", drk? "dark":"light");
|
||||||
|
l.setItem('lightmode', drk? 0:1);
|
||||||
};
|
};
|
||||||
if (window.localStorage && localStorage.getItem('lightmode') != 1)
|
f();
|
||||||
fun();
|
return f;
|
||||||
|
|
||||||
return fun;
|
|
||||||
})();
|
})();
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ function set_jumpto() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function jumpto(ev) {
|
function jumpto(ev) {
|
||||||
var tgt = ev.target || ev.srcElement;
|
var tgt = ev.target;
|
||||||
var ln = null;
|
var ln = null;
|
||||||
while (tgt && !ln) {
|
while (tgt && !ln) {
|
||||||
ln = tgt.getAttribute('data-ln');
|
ln = tgt.getAttribute('data-ln');
|
||||||
@@ -96,26 +96,17 @@ function md_changed(mde, on_srv) {
|
|||||||
var md_now = mde.value();
|
var md_now = mde.value();
|
||||||
var save_btn = QS('.editor-toolbar button.save');
|
var save_btn = QS('.editor-toolbar button.save');
|
||||||
|
|
||||||
if (md_now == window.md_saved)
|
clmod(save_btn, 'disabled', md_now == window.md_saved);
|
||||||
save_btn.classList.add('disabled');
|
|
||||||
else
|
|
||||||
save_btn.classList.remove('disabled');
|
|
||||||
|
|
||||||
set_jumpto();
|
set_jumpto();
|
||||||
}
|
}
|
||||||
|
|
||||||
function save(mde) {
|
function save(mde) {
|
||||||
var save_btn = QS('.editor-toolbar button.save');
|
var save_btn = QS('.editor-toolbar button.save');
|
||||||
if (save_btn.classList.contains('disabled')) {
|
if (clgot(save_btn, 'disabled'))
|
||||||
alert('there is nothing to save');
|
return toast.inf(2, 'no changes');
|
||||||
return;
|
|
||||||
}
|
|
||||||
var force = save_btn.classList.contains('force-save');
|
|
||||||
if (force && !confirm('confirm that you wish to lose the changes made on the server since you opened this document')) {
|
|
||||||
alert('ok, aborted');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
var force = clgot(save_btn, 'force-save');
|
||||||
|
function save2() {
|
||||||
var txt = mde.value();
|
var txt = mde.value();
|
||||||
|
|
||||||
var fd = new FormData();
|
var fd = new FormData();
|
||||||
@@ -134,27 +125,32 @@ function save(mde) {
|
|||||||
xhr.send(fd);
|
xhr.send(fd);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!force)
|
||||||
|
save2();
|
||||||
|
else
|
||||||
|
modal.confirm('confirm that you wish to lose the changes made on the server since you opened this document', save2, function () {
|
||||||
|
toast.inf(3, 'aborted');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function save_cb() {
|
function save_cb() {
|
||||||
if (this.readyState != XMLHttpRequest.DONE)
|
if (this.readyState != XMLHttpRequest.DONE)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (this.status !== 200) {
|
if (this.status !== 200)
|
||||||
alert('Error! The file was NOT saved.\n\n' + this.status + ": " + (this.responseText + '').replace(/^<pre>/, ""));
|
return toast.err(0, 'Error! The file was NOT saved.\n\n' + this.status + ": " + (this.responseText + '').replace(/^<pre>/, ""));
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var r;
|
var r;
|
||||||
try {
|
try {
|
||||||
r = JSON.parse(this.responseText);
|
r = JSON.parse(this.responseText);
|
||||||
}
|
}
|
||||||
catch (ex) {
|
catch (ex) {
|
||||||
alert('Failed to parse reply from server:\n\n' + this.responseText);
|
return toast.err(0, 'Failed to parse reply from server:\n\n' + this.responseText);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!r.ok) {
|
if (!r.ok) {
|
||||||
if (!this.btn.classList.contains('force-save')) {
|
if (!clgot(this.btn, 'force-save')) {
|
||||||
this.btn.classList.add('force-save');
|
clmod(this.btn, 'force-save', 1);
|
||||||
var msg = [
|
var msg = [
|
||||||
'This file has been modified since you started editing it!\n',
|
'This file has been modified since you started editing it!\n',
|
||||||
'if you really want to overwrite, press save again.\n',
|
'if you really want to overwrite, press save again.\n',
|
||||||
@@ -164,15 +160,13 @@ function save_cb() {
|
|||||||
r.lastmod + ' lastmod on the server now,',
|
r.lastmod + ' lastmod on the server now,',
|
||||||
r.now + ' server time now,\n',
|
r.now + ' server time now,\n',
|
||||||
];
|
];
|
||||||
alert(msg.join('\n'));
|
return toast.err(0, msg.join('\n'));
|
||||||
}
|
}
|
||||||
else {
|
else
|
||||||
alert('Error! Save failed. Maybe this JSON explains why:\n\n' + this.responseText);
|
return toast.err(0, 'Error! Save failed. Maybe this JSON explains why:\n\n' + this.responseText);
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.btn.classList.remove('force-save');
|
clmod(this.btn, 'force-save');
|
||||||
//alert('save OK -- wrote ' + r.size + ' bytes.\n\nsha512: ' + r.sha512);
|
//alert('save OK -- wrote ' + r.size + ' bytes.\n\nsha512: ' + r.sha512);
|
||||||
|
|
||||||
// download the saved doc from the server and compare
|
// download the saved doc from the server and compare
|
||||||
@@ -192,35 +186,23 @@ function save_chk() {
|
|||||||
if (this.readyState != XMLHttpRequest.DONE)
|
if (this.readyState != XMLHttpRequest.DONE)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (this.status !== 200) {
|
if (this.status !== 200)
|
||||||
alert('Error! The file was NOT saved.\n\n' + this.status + ": " + (this.responseText + '').replace(/^<pre>/, ""));
|
return toast.err(0, 'Error! The file was NOT saved.\n\n' + this.status + ": " + (this.responseText + '').replace(/^<pre>/, ""));
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var doc1 = this.txt.replace(/\r\n/g, "\n");
|
var doc1 = this.txt.replace(/\r\n/g, "\n");
|
||||||
var doc2 = this.responseText.replace(/\r\n/g, "\n");
|
var doc2 = this.responseText.replace(/\r\n/g, "\n");
|
||||||
if (doc1 != doc2) {
|
if (doc1 != doc2) {
|
||||||
alert(
|
modal.alert(
|
||||||
'Error! The document on the server does not appear to have saved correctly (your editor contents and the server copy is not identical). Place the document on your clipboard for now and check the server logs for hints\n\n' +
|
'Error! The document on the server does not appear to have saved correctly (your editor contents and the server copy is not identical). Place the document on your clipboard for now and check the server logs for hints\n\n' +
|
||||||
'Length: yours=' + doc1.length + ', server=' + doc2.length
|
'Length: yours=' + doc1.length + ', server=' + doc2.length
|
||||||
);
|
);
|
||||||
alert('yours, ' + doc1.length + ' byte:\n[' + doc1 + ']');
|
modal.alert('yours, ' + doc1.length + ' byte:\n[' + doc1 + ']');
|
||||||
alert('server, ' + doc2.length + ' byte:\n[' + doc2 + ']');
|
modal.alert('server, ' + doc2.length + ' byte:\n[' + doc2 + ']');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
last_modified = this.lastmod;
|
last_modified = this.lastmod;
|
||||||
md_changed(this.mde, true);
|
md_changed(this.mde, true);
|
||||||
|
|
||||||
var ok = mknod('div');
|
toast.ok(2, 'save OK' + (this.ntry ? '\nattempt ' + this.ntry : ''));
|
||||||
ok.setAttribute('style', 'font-size:6em;font-family:serif;font-weight:bold;color:#cf6;background:#444;border-radius:.3em;padding:.6em 0;position:fixed;top:30%;left:calc(50% - 2em);width:4em;text-align:center;z-index:9001;transition:opacity 0.2s ease-in-out;opacity:1');
|
|
||||||
ok.innerHTML = 'OK✔️';
|
|
||||||
var parent = ebi('m');
|
|
||||||
document.documentElement.appendChild(ok);
|
|
||||||
setTimeout(function () {
|
|
||||||
ok.style.opacity = 0;
|
|
||||||
}, 500);
|
|
||||||
setTimeout(function () {
|
|
||||||
ok.parentNode.removeChild(ok);
|
|
||||||
}, 750);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,14 +11,12 @@ html {
|
|||||||
background: #333;
|
background: #333;
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
text-shadow: 1px 1px 0px #000;
|
text-shadow: 1px 1px 0px #000;
|
||||||
|
touch-action: manipulation;
|
||||||
}
|
}
|
||||||
html, body {
|
html, body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
body {
|
|
||||||
padding-bottom: 5em;
|
|
||||||
}
|
|
||||||
#box {
|
#box {
|
||||||
padding: .5em 1em;
|
padding: .5em 1em;
|
||||||
background: #2c2c2c;
|
background: #2c2c2c;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<title>copyparty</title>
|
<title>copyparty</title>
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=0.8">
|
<meta name="viewport" content="width=device-width, initial-scale=0.8">
|
||||||
<link rel="stylesheet" type="text/css" media="screen" href="/.cpr/msg.css?_={{ ts }}">
|
<link rel="stylesheet" media="screen" href="/.cpr/msg.css?_={{ ts }}">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ html, body, #wrap {
|
|||||||
background: #f7f7f7;
|
background: #f7f7f7;
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
}
|
}
|
||||||
|
html {
|
||||||
|
touch-action: manipulation;
|
||||||
|
}
|
||||||
#wrap {
|
#wrap {
|
||||||
max-width: 40em;
|
max-width: 40em;
|
||||||
margin: 2em auto;
|
margin: 2em auto;
|
||||||
@@ -26,6 +29,12 @@ a {
|
|||||||
border-radius: .2em;
|
border-radius: .2em;
|
||||||
padding: .2em .8em;
|
padding: .2em .8em;
|
||||||
}
|
}
|
||||||
|
#repl {
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
color: inherit;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
table {
|
table {
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,8 @@
|
|||||||
<title>copyparty</title>
|
<title>copyparty</title>
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=0.8">
|
<meta name="viewport" content="width=device-width, initial-scale=0.8">
|
||||||
<link rel="stylesheet" type="text/css" media="screen" href="/.cpr/splash.css?_={{ ts }}">
|
<link rel="stylesheet" media="screen" href="/.cpr/splash.css?_={{ ts }}">
|
||||||
|
<link rel="stylesheet" media="screen" href="/.cpr/ui.css?_={{ ts }}">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
@@ -66,11 +67,13 @@
|
|||||||
</form>
|
</form>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
<a href="#" id="repl">π</a>
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
if (window.localStorage && localStorage.getItem('lightmode') != 1)
|
if (localStorage.getItem('lightmode') != 1)
|
||||||
document.documentElement.setAttribute("class", "dark");
|
document.documentElement.setAttribute("class", "dark");
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
<script src="/.cpr/util.js?_={{ ts }}"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
442
copyparty/web/ui.css
Normal file
442
copyparty/web/ui.css
Normal file
@@ -0,0 +1,442 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: 'scp';
|
||||||
|
src: local('Source Code Pro Regular'), local('SourceCodePro-Regular'), url(/.cpr/deps/scp.woff2) format('woff2');
|
||||||
|
}
|
||||||
|
html {
|
||||||
|
touch-action: manipulation;
|
||||||
|
}
|
||||||
|
#tt, #toast {
|
||||||
|
position: fixed;
|
||||||
|
max-width: 34em;
|
||||||
|
max-width: min(34em, 90%);
|
||||||
|
max-width: min(34em, calc(100% - 7em));
|
||||||
|
background: #222;
|
||||||
|
border: 0 solid #777;
|
||||||
|
box-shadow: 0 .2em .5em #222;
|
||||||
|
border-radius: .4em;
|
||||||
|
z-index: 9001;
|
||||||
|
}
|
||||||
|
#tt {
|
||||||
|
max-width: min(34em, calc(100% - 3.3em));
|
||||||
|
overflow: hidden;
|
||||||
|
margin: .7em 0;
|
||||||
|
padding: 0 1.3em;
|
||||||
|
height: 0;
|
||||||
|
opacity: .1;
|
||||||
|
transition: opacity 0.14s, height 0.14s, padding 0.14s;
|
||||||
|
}
|
||||||
|
#toast {
|
||||||
|
bottom: 5em;
|
||||||
|
right: -1em;
|
||||||
|
line-height: 1.5em;
|
||||||
|
padding: 1em 1.3em;
|
||||||
|
margin-left: 3em;
|
||||||
|
border-width: .4em 0;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
transform: translateX(100%);
|
||||||
|
transition:
|
||||||
|
transform .4s cubic-bezier(.2, 1.2, .5, 1),
|
||||||
|
right .4s cubic-bezier(.2, 1.2, .5, 1);
|
||||||
|
text-shadow: 1px 1px 0 #000;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
#toast a {
|
||||||
|
color: inherit;
|
||||||
|
text-shadow: inherit;
|
||||||
|
background: rgba(0, 0, 0, 0.4);
|
||||||
|
border-radius: .3em;
|
||||||
|
padding: .2em .3em;
|
||||||
|
}
|
||||||
|
#toast a#toastc {
|
||||||
|
display: inline-block;
|
||||||
|
position: absolute;
|
||||||
|
overflow: hidden;
|
||||||
|
left: 0;
|
||||||
|
width: 0;
|
||||||
|
opacity: 0;
|
||||||
|
padding: .3em 0;
|
||||||
|
margin: -.3em 0 0 0;
|
||||||
|
line-height: 1.3em;
|
||||||
|
color: #000;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
text-shadow: none;
|
||||||
|
border-radius: .5em 0 0 .5em;
|
||||||
|
transition: left .3s, width .3s, padding .3s, opacity .3s;
|
||||||
|
}
|
||||||
|
#toastb {
|
||||||
|
max-height: 70vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
#toast.scroll #toastb {
|
||||||
|
overflow-y: scroll;
|
||||||
|
margin-right: -1.2em;
|
||||||
|
padding-right: .7em;
|
||||||
|
}
|
||||||
|
#toast pre {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
#toast.vis {
|
||||||
|
right: 1.3em;
|
||||||
|
transform: unset;
|
||||||
|
}
|
||||||
|
#toast.vis #toastc {
|
||||||
|
left: -2em;
|
||||||
|
width: .4em;
|
||||||
|
padding: .3em .8em;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
#toast.inf {
|
||||||
|
background: #07a;
|
||||||
|
border-color: #0be;
|
||||||
|
}
|
||||||
|
#toast.inf #toastc {
|
||||||
|
background: #0be;
|
||||||
|
}
|
||||||
|
#toast.ok {
|
||||||
|
background: #380;
|
||||||
|
border-color: #8e4;
|
||||||
|
}
|
||||||
|
#toast.ok #toastc {
|
||||||
|
background: #8e4;
|
||||||
|
}
|
||||||
|
#toast.warn {
|
||||||
|
background: #960;
|
||||||
|
border-color: #fc0;
|
||||||
|
}
|
||||||
|
#toast.warn #toastc {
|
||||||
|
background: #fc0;
|
||||||
|
}
|
||||||
|
#toast.err {
|
||||||
|
background: #900;
|
||||||
|
border-color: #d06;
|
||||||
|
}
|
||||||
|
#toast.err #toastc {
|
||||||
|
background: #d06;
|
||||||
|
}
|
||||||
|
#tt.b {
|
||||||
|
padding: 0 2em;
|
||||||
|
border-radius: .5em;
|
||||||
|
box-shadow: 0 .2em 1em #000;
|
||||||
|
}
|
||||||
|
#tt.show {
|
||||||
|
padding: 1em 1.3em;
|
||||||
|
border-width: .4em 0;
|
||||||
|
height: auto;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
#tt.show.b {
|
||||||
|
padding: 1.5em 2em;
|
||||||
|
border-width: .5em 0;
|
||||||
|
}
|
||||||
|
#modalc code,
|
||||||
|
#tt code {
|
||||||
|
background: #3c3c3c;
|
||||||
|
padding: .1em .3em;
|
||||||
|
border-top: 1px solid #777;
|
||||||
|
border-radius: .3em;
|
||||||
|
line-height: 1.7em;
|
||||||
|
}
|
||||||
|
#tt em {
|
||||||
|
color: #f6a;
|
||||||
|
}
|
||||||
|
html.light #tt {
|
||||||
|
background: #fff;
|
||||||
|
border-color: #888 #000 #777 #000;
|
||||||
|
}
|
||||||
|
html.light #tt,
|
||||||
|
html.light #toast {
|
||||||
|
box-shadow: 0 .3em 1em rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
#modalc code,
|
||||||
|
html.light #tt code {
|
||||||
|
background: #060;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
html.light #tt em {
|
||||||
|
color: #d38;
|
||||||
|
}
|
||||||
|
#modal {
|
||||||
|
position: fixed;
|
||||||
|
overflow: auto;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 9001;
|
||||||
|
background: rgba(64,64,64,0.6);
|
||||||
|
}
|
||||||
|
#modal>table {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
#modal td {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
#modalc {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
background: #f7f7f7;
|
||||||
|
color: #333;
|
||||||
|
text-shadow: none;
|
||||||
|
text-align: left;
|
||||||
|
margin: 3em;
|
||||||
|
padding: 1em 1.1em;
|
||||||
|
border-radius: .6em;
|
||||||
|
box-shadow: 0 .3em 3em rgba(0,0,0,0.5);
|
||||||
|
max-width: 50em;
|
||||||
|
max-height: 30em;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
@media (min-width: 40em) {
|
||||||
|
#modalc {
|
||||||
|
min-width: 30em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#modalc li {
|
||||||
|
margin: 1em 0;
|
||||||
|
}
|
||||||
|
#modalc h6 {
|
||||||
|
font-size: 1.3em;
|
||||||
|
border-bottom: 1px solid #999;
|
||||||
|
margin: 0;
|
||||||
|
padding: .3em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
#modalb {
|
||||||
|
position: sticky;
|
||||||
|
text-align: right;
|
||||||
|
padding-top: 1em;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
#modalb a {
|
||||||
|
color: #000;
|
||||||
|
background: #ccc;
|
||||||
|
display: inline-block;
|
||||||
|
border-radius: .3em;
|
||||||
|
padding: .5em 1em;
|
||||||
|
outline: none;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
#modalb a:focus,
|
||||||
|
#modalb a:hover {
|
||||||
|
background: #06d;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
#modalb a+a {
|
||||||
|
margin-left: .5em;
|
||||||
|
}
|
||||||
|
#modali {
|
||||||
|
display: block;
|
||||||
|
background: #fff;
|
||||||
|
color: #000;
|
||||||
|
width: calc(100% - 1.25em);
|
||||||
|
margin: 1em -.1em 0 -.1em;
|
||||||
|
padding: .5em;
|
||||||
|
outline: none;
|
||||||
|
border: .25em solid #ccc;
|
||||||
|
border-radius: .4em;
|
||||||
|
}
|
||||||
|
#modali:focus {
|
||||||
|
border-color: #06d;
|
||||||
|
}
|
||||||
|
#repl_pre {
|
||||||
|
max-width: 24em;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.mdo pre,
|
||||||
|
.mdo code,
|
||||||
|
.mdo a {
|
||||||
|
color: #480;
|
||||||
|
background: #f7f7f7;
|
||||||
|
border: .07em solid #ddd;
|
||||||
|
border-radius: .2em;
|
||||||
|
padding: .1em .3em;
|
||||||
|
margin: 0 .1em;
|
||||||
|
}
|
||||||
|
.mdo pre,
|
||||||
|
.mdo code,
|
||||||
|
.mdo tt {
|
||||||
|
font-family: 'scp', monospace, monospace;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
.mdo code {
|
||||||
|
font-size: .96em;
|
||||||
|
}
|
||||||
|
.mdo h1,
|
||||||
|
.mdo h2 {
|
||||||
|
line-height: 1.5em;
|
||||||
|
}
|
||||||
|
.mdo h1 {
|
||||||
|
font-size: 1.7em;
|
||||||
|
text-align: center;
|
||||||
|
border: 1em solid #777;
|
||||||
|
border-width: .05em 0;
|
||||||
|
margin: 3em 0;
|
||||||
|
}
|
||||||
|
.mdo h2 {
|
||||||
|
font-size: 1.5em;
|
||||||
|
font-weight: normal;
|
||||||
|
background: #f7f7f7;
|
||||||
|
border-top: .07em solid #fff;
|
||||||
|
border-bottom: .07em solid #bbb;
|
||||||
|
border-radius: .5em .5em 0 0;
|
||||||
|
padding-left: .4em;
|
||||||
|
margin-top: 3em;
|
||||||
|
}
|
||||||
|
.mdo h3 {
|
||||||
|
border-bottom: .1em solid #999;
|
||||||
|
}
|
||||||
|
.mdo h1 a, .mdo h3 a, .mdo h5 a,
|
||||||
|
.mdo h2 a, .mdo h4 a, .mdo h6 a {
|
||||||
|
color: inherit;
|
||||||
|
display: block;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.mdo ul,
|
||||||
|
.mdo ol {
|
||||||
|
border-left: .3em solid #ddd;
|
||||||
|
}
|
||||||
|
.mdo ul>li,
|
||||||
|
.mdo ol>li {
|
||||||
|
margin: .7em 0;
|
||||||
|
list-style-type: disc;
|
||||||
|
}
|
||||||
|
.mdo strong {
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
.mdo p>em,
|
||||||
|
.mdo li>em,
|
||||||
|
.mdo td>em {
|
||||||
|
color: #c50;
|
||||||
|
padding: .1em;
|
||||||
|
border-bottom: .1em solid #bbb;
|
||||||
|
}
|
||||||
|
.mdo blockquote {
|
||||||
|
font-family: serif;
|
||||||
|
background: #f7f7f7;
|
||||||
|
border: .07em dashed #ccc;
|
||||||
|
padding: 0 2em;
|
||||||
|
margin: 1em 0;
|
||||||
|
}
|
||||||
|
.mdo small {
|
||||||
|
opacity: .8;
|
||||||
|
}
|
||||||
|
.mdo pre code {
|
||||||
|
display: block;
|
||||||
|
margin: 0 -.3em;
|
||||||
|
padding: .4em .5em;
|
||||||
|
line-height: 1.1em;
|
||||||
|
}
|
||||||
|
.mdo pre code:hover {
|
||||||
|
background: #fec;
|
||||||
|
color: #360;
|
||||||
|
}
|
||||||
|
.mdo table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 1em 0;
|
||||||
|
}
|
||||||
|
.mdo th,
|
||||||
|
.mdo td {
|
||||||
|
padding: .2em .5em;
|
||||||
|
border: .12em solid #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen {
|
||||||
|
.mdo {
|
||||||
|
word-break: break-word;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
word-wrap: break-word; /*ie*/
|
||||||
|
}
|
||||||
|
html.light .mdo a,
|
||||||
|
.mdo a {
|
||||||
|
color: #fff;
|
||||||
|
background: #39b;
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 0 .3em;
|
||||||
|
border: none;
|
||||||
|
border-bottom: .07em solid #079;
|
||||||
|
}
|
||||||
|
.mdo h1 {
|
||||||
|
color: #fff;
|
||||||
|
background: #444;
|
||||||
|
font-weight: normal;
|
||||||
|
border-top: .4em solid #fb0;
|
||||||
|
border-bottom: .4em solid #777;
|
||||||
|
border-radius: 0 1em 0 1em;
|
||||||
|
margin: 3em 0 1em 0;
|
||||||
|
padding: .5em 0;
|
||||||
|
}
|
||||||
|
.mdo h2 {
|
||||||
|
color: #fff;
|
||||||
|
background: #555;
|
||||||
|
margin-top: 2em;
|
||||||
|
border-bottom: .22em solid #999;
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
html.dark .mdo a {
|
||||||
|
background: #057;
|
||||||
|
}
|
||||||
|
html.dark .mdo h1 a, html.dark .mdo h4 a,
|
||||||
|
html.dark .mdo h2 a, html.dark .mdo h5 a,
|
||||||
|
html.dark .mdo h3 a, html.dark .mdo h6 a {
|
||||||
|
color: inherit;
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
html.dark .mdo pre,
|
||||||
|
html.dark .mdo code {
|
||||||
|
color: #8c0;
|
||||||
|
background: #1a1a1a;
|
||||||
|
border: .07em solid #333;
|
||||||
|
}
|
||||||
|
html.dark .mdo ul,
|
||||||
|
html.dark .mdo ol {
|
||||||
|
border-color: #444;
|
||||||
|
}
|
||||||
|
html.dark .mdo strong {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
html.dark .mdo p>em,
|
||||||
|
html.dark .mdo li>em,
|
||||||
|
html.dark .mdo td>em {
|
||||||
|
color: #f94;
|
||||||
|
border-color: #666;
|
||||||
|
}
|
||||||
|
html.dark .mdo h1 {
|
||||||
|
background: #383838;
|
||||||
|
border-top: .4em solid #b80;
|
||||||
|
border-bottom: .4em solid #4c4c4c;
|
||||||
|
}
|
||||||
|
html.dark .mdo h2 {
|
||||||
|
background: #444;
|
||||||
|
border-bottom: .22em solid #555;
|
||||||
|
}
|
||||||
|
html.dark .mdo td,
|
||||||
|
html.dark .mdo th {
|
||||||
|
border-color: #444;
|
||||||
|
}
|
||||||
|
html.dark .mdo blockquote {
|
||||||
|
background: #282828;
|
||||||
|
border: .07em dashed #444;
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,296 +0,0 @@
|
|||||||
|
|
||||||
#op_up2k {
|
|
||||||
padding: 0 1em 1em 1em;
|
|
||||||
}
|
|
||||||
#u2form {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 2px;
|
|
||||||
height: 2px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
#u2form input {
|
|
||||||
background: #444;
|
|
||||||
border: 0px solid #444;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
#u2err.err {
|
|
||||||
color: #f87;
|
|
||||||
padding: .5em;
|
|
||||||
}
|
|
||||||
#u2err.msg {
|
|
||||||
color: #999;
|
|
||||||
padding: .5em;
|
|
||||||
font-size: .9em;
|
|
||||||
}
|
|
||||||
#u2btn {
|
|
||||||
color: #eee;
|
|
||||||
background: #555;
|
|
||||||
background: -moz-linear-gradient(top, #367 0%, #489 50%, #38788a 51%, #367 100%);
|
|
||||||
background: -webkit-linear-gradient(top, #367 0%, #489 50%, #38788a 51%, #367 100%);
|
|
||||||
background: linear-gradient(to bottom, #367 0%, #489 50%, #38788a 51%, #367 100%);
|
|
||||||
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#489', endColorstr='#38788a', GradientType=0);
|
|
||||||
text-decoration: none;
|
|
||||||
line-height: 1.3em;
|
|
||||||
border: 1px solid #222;
|
|
||||||
border-radius: .4em;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 1.5em;
|
|
||||||
margin: .5em auto;
|
|
||||||
padding: .8em 0;
|
|
||||||
width: 16em;
|
|
||||||
cursor: pointer;
|
|
||||||
box-shadow: .4em .4em 0 #111;
|
|
||||||
}
|
|
||||||
#op_up2k.srch #u2btn {
|
|
||||||
background: linear-gradient(to bottom, #ca3 0%, #fd8 50%, #fc6 51%, #b92 100%);
|
|
||||||
text-shadow: 1px 1px 1px #fc6;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
#u2conf #u2btn {
|
|
||||||
margin: -1.5em 0;
|
|
||||||
padding: .8em 0;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 12em;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
#u2conf #u2btn_cw {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
#u2notbtn {
|
|
||||||
display: none;
|
|
||||||
text-align: center;
|
|
||||||
background: #333;
|
|
||||||
padding-top: 1em;
|
|
||||||
}
|
|
||||||
#u2notbtn * {
|
|
||||||
line-height: 1.3em;
|
|
||||||
}
|
|
||||||
#u2tab {
|
|
||||||
margin: 3em auto;
|
|
||||||
width: calc(100% - 2em);
|
|
||||||
max-width: 100em;
|
|
||||||
}
|
|
||||||
#op_up2k.srch #u2tab {
|
|
||||||
max-width: none;
|
|
||||||
}
|
|
||||||
#u2tab td {
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
border-width: 0 0px 1px 0;
|
|
||||||
padding: .1em .3em;
|
|
||||||
}
|
|
||||||
#u2tab td:nth-child(2) {
|
|
||||||
width: 5em;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
#u2tab td:nth-child(3) {
|
|
||||||
width: 40%;
|
|
||||||
}
|
|
||||||
#op_up2k.srch #u2tab td:nth-child(3) {
|
|
||||||
font-family: sans-serif;
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
#u2tab tbody tr:hover td {
|
|
||||||
background: #222;
|
|
||||||
}
|
|
||||||
#u2cards {
|
|
||||||
padding: 1em 0 .3em 1em;
|
|
||||||
margin: 1.5em auto -2.5em auto;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-align: center;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
#u2cards.w {
|
|
||||||
width: 45em;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
#u2cards a {
|
|
||||||
padding: .2em 1em;
|
|
||||||
border: 1px solid #777;
|
|
||||||
border-width: 0 0 1px 0;
|
|
||||||
background: linear-gradient(to bottom, #333, #222);
|
|
||||||
}
|
|
||||||
#u2cards a:first-child {
|
|
||||||
border-radius: .4em 0 0 0;
|
|
||||||
}
|
|
||||||
#u2cards a:last-child {
|
|
||||||
border-radius: 0 .4em 0 0;
|
|
||||||
}
|
|
||||||
#u2cards a.act {
|
|
||||||
padding-bottom: .5em;
|
|
||||||
border-width: 1px 1px .1em 1px;
|
|
||||||
border-radius: .3em .3em 0 0;
|
|
||||||
margin-left: -1px;
|
|
||||||
background: linear-gradient(to bottom, #464, #333 80%);
|
|
||||||
box-shadow: 0 -.17em .67em #280;
|
|
||||||
border-color: #7c5 #583 #333 #583;
|
|
||||||
position: relative;
|
|
||||||
color: #fd7;
|
|
||||||
}
|
|
||||||
#u2cards span {
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
#u2conf {
|
|
||||||
margin: 1em auto;
|
|
||||||
width: 30em;
|
|
||||||
}
|
|
||||||
#u2conf.has_btn {
|
|
||||||
width: 48em;
|
|
||||||
}
|
|
||||||
#u2conf * {
|
|
||||||
text-align: center;
|
|
||||||
line-height: 1em;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
border: none;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
#u2conf .txtbox {
|
|
||||||
width: 3em;
|
|
||||||
color: #fff;
|
|
||||||
background: #444;
|
|
||||||
border: 1px solid #777;
|
|
||||||
font-size: 1.2em;
|
|
||||||
padding: .15em 0;
|
|
||||||
height: 1.05em;
|
|
||||||
}
|
|
||||||
#u2conf .txtbox.err {
|
|
||||||
background: #922;
|
|
||||||
}
|
|
||||||
#u2conf a {
|
|
||||||
color: #fff;
|
|
||||||
background: #c38;
|
|
||||||
text-decoration: none;
|
|
||||||
border-radius: .1em;
|
|
||||||
font-size: 1.5em;
|
|
||||||
padding: .1em 0;
|
|
||||||
margin: 0 -1px;
|
|
||||||
width: 1.5em;
|
|
||||||
height: 1em;
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
bottom: -0.08em;
|
|
||||||
}
|
|
||||||
#u2conf input+a {
|
|
||||||
background: #d80;
|
|
||||||
}
|
|
||||||
#u2conf label {
|
|
||||||
font-size: 1.6em;
|
|
||||||
width: 2em;
|
|
||||||
height: 1em;
|
|
||||||
padding: .4em 0;
|
|
||||||
display: block;
|
|
||||||
border-radius: .25em;
|
|
||||||
}
|
|
||||||
#u2conf input[type="checkbox"] {
|
|
||||||
position: relative;
|
|
||||||
opacity: .02;
|
|
||||||
top: 2em;
|
|
||||||
}
|
|
||||||
#u2conf input[type="checkbox"]+label {
|
|
||||||
position: relative;
|
|
||||||
background: #603;
|
|
||||||
border-bottom: .2em solid #a16;
|
|
||||||
box-shadow: 0 .1em .3em #a00 inset;
|
|
||||||
}
|
|
||||||
#u2conf input[type="checkbox"]:checked+label {
|
|
||||||
background: #6a1;
|
|
||||||
border-bottom: .2em solid #efa;
|
|
||||||
box-shadow: 0 .1em .5em #0c0;
|
|
||||||
}
|
|
||||||
#u2conf input[type="checkbox"]+label:hover {
|
|
||||||
box-shadow: 0 .1em .3em #fb0;
|
|
||||||
border-color: #fb0;
|
|
||||||
}
|
|
||||||
#op_up2k.srch #u2conf td:nth-child(1)>*,
|
|
||||||
#op_up2k.srch #u2conf td:nth-child(2)>*,
|
|
||||||
#op_up2k.srch #u2conf td:nth-child(3)>* {
|
|
||||||
background: #777;
|
|
||||||
border-color: #ccc;
|
|
||||||
box-shadow: none;
|
|
||||||
opacity: .2;
|
|
||||||
}
|
|
||||||
#u2foot {
|
|
||||||
color: #fff;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
#u2foot .warn {
|
|
||||||
font-size: 1.3em;
|
|
||||||
padding: .5em .8em;
|
|
||||||
margin: 1em -.6em;
|
|
||||||
color: #f74;
|
|
||||||
background: #322;
|
|
||||||
border: 1px solid #633;
|
|
||||||
border-width: .1em 0;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
#u2foot .warn span {
|
|
||||||
color: #f86;
|
|
||||||
}
|
|
||||||
html.light #u2foot .warn {
|
|
||||||
color: #b00;
|
|
||||||
background: #fca;
|
|
||||||
border-color: #f70;
|
|
||||||
}
|
|
||||||
html.light #u2foot .warn span {
|
|
||||||
color: #930;
|
|
||||||
}
|
|
||||||
#u2foot span {
|
|
||||||
color: #999;
|
|
||||||
font-size: .9em;
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
#u2footfoot {
|
|
||||||
margin-bottom: -1em;
|
|
||||||
}
|
|
||||||
.prog {
|
|
||||||
font-family: monospace;
|
|
||||||
}
|
|
||||||
#u2tab a>span {
|
|
||||||
font-weight: bold;
|
|
||||||
font-style: italic;
|
|
||||||
color: #fff;
|
|
||||||
padding-left: .2em;
|
|
||||||
}
|
|
||||||
#u2cleanup {
|
|
||||||
float: right;
|
|
||||||
margin-bottom: -.3em;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
html.light #u2btn {
|
|
||||||
box-shadow: .4em .4em 0 #ccc;
|
|
||||||
}
|
|
||||||
html.light #u2cards span {
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
html.light #u2cards a {
|
|
||||||
background: linear-gradient(to bottom, #eee, #fff);
|
|
||||||
}
|
|
||||||
html.light #u2cards a.act {
|
|
||||||
color: #037;
|
|
||||||
background: inherit;
|
|
||||||
box-shadow: 0 -.17em .67em #0ad;
|
|
||||||
border-color: #09c #05a #eee #05a;
|
|
||||||
}
|
|
||||||
html.light #u2conf .txtbox {
|
|
||||||
background: #fff;
|
|
||||||
color: #444;
|
|
||||||
}
|
|
||||||
html.light #u2conf .txtbox.err {
|
|
||||||
background: #f96;
|
|
||||||
color: #300;
|
|
||||||
}
|
|
||||||
html.light #op_up2k.srch #u2btn {
|
|
||||||
border-color: #a80;
|
|
||||||
}
|
|
||||||
html.light #u2foot {
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
html.light #u2tab tbody tr:hover td {
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,15 @@ if (!window['console'])
|
|||||||
|
|
||||||
|
|
||||||
var is_touch = 'ontouchstart' in window,
|
var is_touch = 'ontouchstart' in window,
|
||||||
ANDROID = /(android)/i.test(navigator.userAgent);
|
IPHONE = /iPhone|iPad|iPod/i.test(navigator.userAgent),
|
||||||
|
ANDROID = /android/i.test(navigator.userAgent),
|
||||||
|
WINDOWS = navigator.platform ? navigator.platform == 'Win32' : /Windows/.test(navigator.userAgent);
|
||||||
|
|
||||||
|
|
||||||
|
var ebi = document.getElementById.bind(document),
|
||||||
|
QS = document.querySelector.bind(document),
|
||||||
|
QSA = document.querySelectorAll.bind(document),
|
||||||
|
mknod = document.createElement.bind(document);
|
||||||
|
|
||||||
|
|
||||||
// error handler for mobile devices
|
// error handler for mobile devices
|
||||||
@@ -21,36 +29,140 @@ function esc(txt) {
|
|||||||
}[c];
|
}[c];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
window.onunhandledrejection = function (e) {
|
||||||
|
console.log("REJ: " + e.reason);
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
console.hist = [];
|
||||||
|
var hook = function (t) {
|
||||||
|
var orig = console[t].bind(console),
|
||||||
|
cfun = function () {
|
||||||
|
console.hist.push(Date.now() + ' ' + t + ': ' + Array.from(arguments).join(', '));
|
||||||
|
if (console.hist.length > 100)
|
||||||
|
console.hist = console.hist.slice(50);
|
||||||
|
|
||||||
|
orig.apply(console, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
console['std' + t] = orig;
|
||||||
|
console[t] = cfun;
|
||||||
|
};
|
||||||
|
hook('log');
|
||||||
|
console.log('log-capture ok');
|
||||||
|
hook('debug');
|
||||||
|
hook('warn');
|
||||||
|
hook('error');
|
||||||
|
}
|
||||||
|
catch (ex) {
|
||||||
|
if (console.stdlog)
|
||||||
|
console.log = console.stdlog;
|
||||||
|
console.log(ex);
|
||||||
|
}
|
||||||
|
var crashed = false, ignexd = {};
|
||||||
function vis_exh(msg, url, lineNo, columnNo, error) {
|
function vis_exh(msg, url, lineNo, columnNo, error) {
|
||||||
if (!window.onerror)
|
if ((msg + '').indexOf('ResizeObserver') !== -1)
|
||||||
|
return; // chrome issue 809574 (benign, from <video>)
|
||||||
|
|
||||||
|
var ekey = url + '\n' + lineNo + '\n' + msg;
|
||||||
|
if (ignexd[ekey] || crashed)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
crashed = true;
|
||||||
window.onerror = undefined;
|
window.onerror = undefined;
|
||||||
window['vis_exh'] = null;
|
var html = [
|
||||||
var html = ['<h1>you hit a bug!</h1><p>please send me a screenshot arigathanks gozaimuch: <code>ed/irc.rizon.net</code> or <code>ed#2644</code><br /> (and if you can, press F12 and include the "Console" tab in the screenshot too)</p><p>',
|
'<h1>you hit a bug!</h1>',
|
||||||
esc(String(msg)), '</p><p>', esc(url + ' @' + lineNo + ':' + columnNo), '</p>'];
|
'<p style="font-size:1.3em;margin:0">try to <a href="#" onclick="localStorage.clear();location.reload();">reset copyparty settings</a> if you are stuck here, or <a href="#" onclick="ignex();">ignore this</a> / <a href="#" onclick="ignex(true);">ignore all</a></p>',
|
||||||
|
'<p style="color:#fff">please send me a screenshot arigathanks gozaimuch: <a href="<ghi>" target="_blank">github issue</a> or <code>ed#2644</code></p>',
|
||||||
|
'<p class="b">' + esc(url + ' @' + lineNo + ':' + columnNo), '<br />' + esc(String(msg)) + '</p>',
|
||||||
|
'<p><b>UA:</b> ' + esc(navigator.userAgent + '')
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
var ua = '',
|
||||||
|
ad = navigator.userAgentData,
|
||||||
|
adb = ad.brands;
|
||||||
|
|
||||||
|
for (var a = 0; a < adb.length; a++)
|
||||||
|
if (!/Not.*A.*Brand/.exec(adb[a].brand))
|
||||||
|
ua += adb[a].brand + '/' + adb[a].version + ', ';
|
||||||
|
ua += ad.platform;
|
||||||
|
|
||||||
|
html.push('<br /><b>UAD:</b> ' + esc(ua.slice(0, 100)));
|
||||||
|
}
|
||||||
|
catch (e) { }
|
||||||
|
html.push('</p>');
|
||||||
|
|
||||||
|
try {
|
||||||
if (error) {
|
if (error) {
|
||||||
var find = ['desc', 'stack', 'trace'];
|
var find = ['desc', 'stack', 'trace'];
|
||||||
for (var a = 0; a < find.length; a++)
|
for (var a = 0; a < find.length; a++)
|
||||||
if (String(error[find[a]]) !== 'undefined')
|
if (String(error[find[a]]) !== 'undefined')
|
||||||
html.push('<h2>' + find[a] + '</h2>' +
|
html.push('<p class="b"><b>' + find[a] + ':</b><br />' +
|
||||||
esc(String(error[find[a]])).replace(/\n/g, '<br />\n'));
|
esc(String(error[find[a]])).replace(/\n/g, '<br />\n') + '</p>');
|
||||||
}
|
}
|
||||||
document.body.innerHTML = html.join('\n');
|
ignexd[ekey] = true;
|
||||||
|
|
||||||
|
var ls = jcp(localStorage);
|
||||||
|
if (ls.fman_clip)
|
||||||
|
ls.fman_clip = ls.fman_clip.length + ' items';
|
||||||
|
|
||||||
|
var lsk = Object.keys(ls);
|
||||||
|
lsk.sort();
|
||||||
|
html.push('<p class="b">');
|
||||||
|
for (var a = 0; a < lsk.length; a++)
|
||||||
|
html.push(' <b>' + esc(lsk[a]) + '</b> <code>' + esc(ls[lsk[a]]) + '</code> ');
|
||||||
|
html.push('</p>');
|
||||||
|
}
|
||||||
|
catch (e) { }
|
||||||
|
|
||||||
|
if (console.hist.length) {
|
||||||
|
html.push('<p class="b"><b>console:</b><ul><li>' + Date.now() + ' @</li>');
|
||||||
|
for (var a = console.hist.length - 1, aa = Math.max(0, console.hist.length - 20); a >= aa; a--)
|
||||||
|
html.push('<li>' + esc(console.hist[a]) + '</li>');
|
||||||
|
html.push('</ul>')
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
var exbox = ebi('exbox');
|
||||||
|
if (!exbox) {
|
||||||
|
exbox = mknod('div');
|
||||||
|
exbox.setAttribute('id', 'exbox');
|
||||||
|
document.body.appendChild(exbox);
|
||||||
|
|
||||||
var s = mknod('style');
|
var s = mknod('style');
|
||||||
s.innerHTML = 'body{background:#333;color:#ddd;font-family:sans-serif;font-size:0.8em;padding:0 1em 1em 1em} code{color:#bf7;background:#222;padding:.1em;margin:.2em;font-size:1.1em;font-family:monospace,monospace} *{line-height:1.5em}';
|
s.innerHTML = (
|
||||||
|
'#exbox{background:#333;color:#ddd;font-family:sans-serif;font-size:0.8em;padding:0 1em 1em 1em;z-index:80386;position:fixed;top:0;left:0;right:0;bottom:0;width:100%;height:100%;overflow:auto;width:calc(100% - 2em)} ' +
|
||||||
|
'#exbox,#exbox *{line-height:1.5em;overflow-wrap:break-word} ' +
|
||||||
|
'#exbox code{color:#bf7;background:#222;padding:.1em;margin:.2em;font-size:1.1em;font-family:monospace,monospace} ' +
|
||||||
|
'#exbox a{text-decoration:underline;color:#fc0} ' +
|
||||||
|
'#exbox h1{margin:.5em 1em 0 0;padding:0} ' +
|
||||||
|
'#exbox p.b{border-top:1px solid #999;margin:1em 0 0 0;font-size:1em} ' +
|
||||||
|
'#exbox ul, #exbox li {margin:0 0 0 .5em;padding:0} ' +
|
||||||
|
'#exbox b{color:#fff}'
|
||||||
|
);
|
||||||
document.head.appendChild(s);
|
document.head.appendChild(s);
|
||||||
|
}
|
||||||
|
exbox.innerHTML = html.join('\n').replace(/https?:\/\/[^ \/]+\//g, '/').replace(/js\?_=[a-zA-Z]{4}/g, 'js').replace(/<ghi>/, 'https://github.com/9001/copyparty/issues/new?labels=bug&template=bug_report.md');
|
||||||
|
exbox.style.display = 'block';
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
document.body.innerHTML = html.join('\n');
|
||||||
|
}
|
||||||
throw 'fatal_err';
|
throw 'fatal_err';
|
||||||
}
|
}
|
||||||
|
function ignex(all) {
|
||||||
|
var o = ebi('exbox');
|
||||||
|
o.style.display = 'none';
|
||||||
|
o.innerHTML = '';
|
||||||
|
crashed = false;
|
||||||
|
if (!all)
|
||||||
|
window.onerror = vis_exh;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
var ebi = document.getElementById.bind(document),
|
function ctrl(e) {
|
||||||
QS = document.querySelector.bind(document),
|
return e && (e.ctrlKey || e.metaKey);
|
||||||
QSA = document.querySelectorAll.bind(document),
|
}
|
||||||
mknod = document.createElement.bind(document);
|
|
||||||
|
|
||||||
|
|
||||||
function ev(e) {
|
function ev(e) {
|
||||||
@@ -87,6 +199,22 @@ if (!String.startsWith) {
|
|||||||
return this.substring(i, i + s.length) === s;
|
return this.substring(i, i + s.length) === s;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (!Element.prototype.matches) {
|
||||||
|
Element.prototype.matches =
|
||||||
|
Element.prototype.oMatchesSelector ||
|
||||||
|
Element.prototype.msMatchesSelector ||
|
||||||
|
Element.prototype.mozMatchesSelector ||
|
||||||
|
Element.prototype.webkitMatchesSelector;
|
||||||
|
}
|
||||||
|
if (!Element.prototype.closest) {
|
||||||
|
Element.prototype.closest = function (s) {
|
||||||
|
var el = this;
|
||||||
|
do {
|
||||||
|
if (el.matches(s)) return el;
|
||||||
|
el = el.parentElement || el.parentNode;
|
||||||
|
} while (el !== null && el.nodeType === 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// https://stackoverflow.com/a/950146
|
// https://stackoverflow.com/a/950146
|
||||||
@@ -95,10 +223,10 @@ function import_js(url, cb) {
|
|||||||
var script = mknod('script');
|
var script = mknod('script');
|
||||||
script.type = 'text/javascript';
|
script.type = 'text/javascript';
|
||||||
script.src = url;
|
script.src = url;
|
||||||
|
|
||||||
script.onreadystatechange = cb;
|
|
||||||
script.onload = cb;
|
script.onload = cb;
|
||||||
|
script.onerror = function () {
|
||||||
|
toast.err(0, 'Failed to load module:\n' + url);
|
||||||
|
};
|
||||||
head.appendChild(script);
|
head.appendChild(script);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,90 +253,37 @@ function crc32(str) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function clmod(obj, cls, add) {
|
function clmod(el, cls, add) {
|
||||||
var re = new RegExp('\\s*\\b' + cls + '\\s*\\b', 'g');
|
if (el.classList) {
|
||||||
|
var have = el.classList.contains(cls);
|
||||||
if (add == 't')
|
if (add == 't')
|
||||||
add = !re.test(obj.className);
|
add = !have;
|
||||||
|
|
||||||
obj.className = obj.className.replace(re, ' ') + (add ? ' ' + cls : '');
|
if (add != have)
|
||||||
|
el.classList[add ? 'add' : 'remove'](cls);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var re = new RegExp('\\s*\\b' + cls + '\\s*\\b', 'g'),
|
||||||
|
n1 = el.className;
|
||||||
|
|
||||||
|
if (add == 't')
|
||||||
|
add = !re.test(n1);
|
||||||
|
|
||||||
|
var n2 = n1.replace(re, ' ') + (add ? ' ' + cls : '');
|
||||||
|
|
||||||
|
if (n1 != n2)
|
||||||
|
el.className = n2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function sortfiles(nodes) {
|
function clgot(el, cls) {
|
||||||
var sopts = jread('fsort', [["href", 1, ""]]);
|
if (el.classList)
|
||||||
|
return el.classList.contains(cls);
|
||||||
|
|
||||||
try {
|
var lst = (el.getAttribute('class') + '').split(/ /g);
|
||||||
var is_srch = false;
|
return has(lst, cls);
|
||||||
if (nodes[0]['rp']) {
|
|
||||||
is_srch = true;
|
|
||||||
for (var b = 0, bb = nodes.length; b < bb; b++)
|
|
||||||
nodes[b].ext = nodes[b].rp.split('.').pop();
|
|
||||||
for (var b = 0; b < sopts.length; b++)
|
|
||||||
if (sopts[b][0] == 'href')
|
|
||||||
sopts[b][0] = 'rp';
|
|
||||||
}
|
|
||||||
for (var a = sopts.length - 1; a >= 0; a--) {
|
|
||||||
var name = sopts[a][0], rev = sopts[a][1], typ = sopts[a][2];
|
|
||||||
if (!name)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (name == 'ts')
|
|
||||||
typ = 'int';
|
|
||||||
|
|
||||||
if (name.indexOf('tags/') === 0) {
|
|
||||||
name = name.slice(5);
|
|
||||||
for (var b = 0, bb = nodes.length; b < bb; b++)
|
|
||||||
nodes[b]._sv = nodes[b].tags[name];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
for (var b = 0, bb = nodes.length; b < bb; b++) {
|
|
||||||
var v = nodes[b][name];
|
|
||||||
|
|
||||||
if ((v + '').indexOf('<a ') === 0)
|
|
||||||
v = v.split('>')[1];
|
|
||||||
else if (name == "href" && v) {
|
|
||||||
if (v.slice(-1) == '/')
|
|
||||||
v = '\t' + v;
|
|
||||||
|
|
||||||
v = uricom_dec(v)[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
nodes[b]._sv = v;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var onodes = nodes.map(function (x) { return x; });
|
|
||||||
nodes.sort(function (n1, n2) {
|
|
||||||
var v1 = n1._sv,
|
|
||||||
v2 = n2._sv;
|
|
||||||
|
|
||||||
if (v1 === undefined) {
|
|
||||||
if (v2 === undefined) {
|
|
||||||
return onodes.indexOf(n1) - onodes.indexOf(n2);
|
|
||||||
}
|
|
||||||
return -1 * rev;
|
|
||||||
}
|
|
||||||
if (v2 === undefined) return 1 * rev;
|
|
||||||
|
|
||||||
var ret = rev * (typ == 'int' ? (v1 - v2) : (v1.localeCompare(v2)));
|
|
||||||
if (ret === 0)
|
|
||||||
ret = onodes.indexOf(n1) - onodes.indexOf(n2);
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
for (var b = 0, bb = nodes.length; b < bb; b++) {
|
|
||||||
delete nodes[b]._sv;
|
|
||||||
if (is_srch)
|
|
||||||
delete nodes[b].ext;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (ex) {
|
|
||||||
console.log("failed to apply sort config: " + ex);
|
|
||||||
console.log("resetting fsort " + sread('fsort'))
|
|
||||||
localStorage.removeItem('fsort');
|
|
||||||
}
|
|
||||||
return nodes;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -316,6 +391,18 @@ function linksplit(rp) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function vsplit(vp) {
|
||||||
|
if (vp.endsWith('/'))
|
||||||
|
vp = vp.slice(0, -1);
|
||||||
|
|
||||||
|
var ofs = vp.lastIndexOf('/') + 1,
|
||||||
|
base = vp.slice(0, ofs),
|
||||||
|
fn = vp.slice(ofs);
|
||||||
|
|
||||||
|
return [base, fn];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function uricom_enc(txt, do_fb_enc) {
|
function uricom_enc(txt, do_fb_enc) {
|
||||||
try {
|
try {
|
||||||
return encodeURIComponent(txt);
|
return encodeURIComponent(txt);
|
||||||
@@ -329,6 +416,16 @@ function uricom_enc(txt, do_fb_enc) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function url_enc(txt) {
|
||||||
|
var parts = txt.split('/'),
|
||||||
|
ret = [];
|
||||||
|
|
||||||
|
for (var a = 0; a < parts.length; a++)
|
||||||
|
ret.push(uricom_enc(parts[a]));
|
||||||
|
|
||||||
|
return ret.join('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function uricom_dec(txt) {
|
function uricom_dec(txt) {
|
||||||
try {
|
try {
|
||||||
@@ -341,6 +438,17 @@ function uricom_dec(txt) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function uricom_adec(arr, li) {
|
||||||
|
var ret = [];
|
||||||
|
for (var a = 0; a < arr.length; a++) {
|
||||||
|
var txt = uricom_dec(arr[a])[0];
|
||||||
|
ret.push(li ? '<li>' + esc(txt) + '</li>' : txt);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function get_evpath() {
|
function get_evpath() {
|
||||||
var ret = document.location.pathname;
|
var ret = document.location.pathname;
|
||||||
|
|
||||||
@@ -380,6 +488,41 @@ function s2ms(s) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function f2f(val, nd) {
|
||||||
|
// 10.toFixed(1) returns 10.00 for certain values of 10
|
||||||
|
val = (val * Math.pow(10, nd)).toFixed(0).split('.')[0];
|
||||||
|
return nd ? (val.slice(0, -nd) || '0') + '.' + val.slice(-nd) : val;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function humansize(b, terse) {
|
||||||
|
var i = 0, u = terse ? ['B', 'K', 'M', 'G'] : ['B', 'KB', 'MB', 'GB'];
|
||||||
|
while (b >= 1000 && i < u.length) {
|
||||||
|
b /= 1024;
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
return f2f(b, b >= 100 ? 0 : b >= 10 ? 1 : 2) + ' ' + u[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function humantime(v) {
|
||||||
|
if (v >= 60 * 60 * 24)
|
||||||
|
return v;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return /.*(..:..:..).*/.exec(new Date(v * 1000).toUTCString())[1];
|
||||||
|
}
|
||||||
|
catch (ex) {
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function clamp(v, a, b) {
|
||||||
|
return Math.min(Math.max(v, a), b);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function has(haystack, needle) {
|
function has(haystack, needle) {
|
||||||
for (var a = 0; a < haystack.length; a++)
|
for (var a = 0; a < haystack.length; a++)
|
||||||
if (haystack[a] == needle)
|
if (haystack[a] == needle)
|
||||||
@@ -402,20 +545,15 @@ function jcp(obj) {
|
|||||||
|
|
||||||
|
|
||||||
function sread(key) {
|
function sread(key) {
|
||||||
if (window.localStorage)
|
|
||||||
return localStorage.getItem(key);
|
return localStorage.getItem(key);
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function swrite(key, val) {
|
function swrite(key, val) {
|
||||||
if (window.localStorage) {
|
|
||||||
if (val === undefined || val === null)
|
if (val === undefined || val === null)
|
||||||
localStorage.removeItem(key);
|
localStorage.removeItem(key);
|
||||||
else
|
else
|
||||||
localStorage.setItem(key, val);
|
localStorage.setItem(key, val);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
function jread(key, fb) {
|
function jread(key, fb) {
|
||||||
var str = sread(key);
|
var str = sread(key);
|
||||||
@@ -494,16 +632,61 @@ function hist_replace(url) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var timer = (function () {
|
||||||
|
var r = {};
|
||||||
|
r.q = [];
|
||||||
|
r.last = 0;
|
||||||
|
|
||||||
|
r.add = function (fun, run) {
|
||||||
|
r.rm(fun);
|
||||||
|
r.q.push(fun);
|
||||||
|
|
||||||
|
if (run)
|
||||||
|
fun();
|
||||||
|
};
|
||||||
|
|
||||||
|
r.rm = function (fun) {
|
||||||
|
apop(r.q, fun);
|
||||||
|
};
|
||||||
|
|
||||||
|
function doevents() {
|
||||||
|
if (crashed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (Date.now() - r.last < 69)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var q = r.q.slice(0);
|
||||||
|
for (var a = 0; a < q.length; a++)
|
||||||
|
q[a]();
|
||||||
|
|
||||||
|
r.last = Date.now();
|
||||||
|
}
|
||||||
|
setInterval(doevents, 100);
|
||||||
|
|
||||||
|
return r;
|
||||||
|
})();
|
||||||
|
|
||||||
|
|
||||||
var tt = (function () {
|
var tt = (function () {
|
||||||
var r = {
|
var r = {
|
||||||
"tt": mknod("div"),
|
"tt": mknod("div"),
|
||||||
"en": true
|
"en": true,
|
||||||
|
"el": null,
|
||||||
|
"skip": false
|
||||||
};
|
};
|
||||||
|
|
||||||
r.tt.setAttribute('id', 'tt');
|
r.tt.setAttribute('id', 'tt');
|
||||||
document.body.appendChild(r.tt);
|
document.body.appendChild(r.tt);
|
||||||
|
|
||||||
function show() {
|
r.show = function () {
|
||||||
|
if (r.skip) {
|
||||||
|
r.skip = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (QS('body.bbox-open'))
|
||||||
|
return;
|
||||||
|
|
||||||
var cfg = sread('tooltips');
|
var cfg = sread('tooltips');
|
||||||
if (cfg !== null && cfg != '1')
|
if (cfg !== null && cfg != '1')
|
||||||
return;
|
return;
|
||||||
@@ -512,23 +695,83 @@ var tt = (function () {
|
|||||||
if (!msg)
|
if (!msg)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
r.el = this;
|
||||||
var pos = this.getBoundingClientRect(),
|
var pos = this.getBoundingClientRect(),
|
||||||
left = pos.left < window.innerWidth / 2,
|
dir = this.getAttribute('ttd') || '',
|
||||||
|
margin = parseFloat(this.getAttribute('ttm') || 0),
|
||||||
top = pos.top < window.innerHeight / 2,
|
top = pos.top < window.innerHeight / 2,
|
||||||
big = this.className.indexOf(' ttb') !== -1;
|
big = this.className.indexOf(' ttb') !== -1;
|
||||||
|
|
||||||
|
if (dir.indexOf('u') + 1) top = false;
|
||||||
|
if (dir.indexOf('d') + 1) top = true;
|
||||||
|
|
||||||
clmod(r.tt, 'b', big);
|
clmod(r.tt, 'b', big);
|
||||||
r.tt.style.top = top ? pos.bottom + 'px' : 'auto';
|
r.tt.style.left = '0';
|
||||||
r.tt.style.bottom = top ? 'auto' : (window.innerHeight - pos.top) + 'px';
|
r.tt.style.top = '0';
|
||||||
r.tt.style.left = left ? pos.left + 'px' : 'auto';
|
|
||||||
r.tt.style.right = left ? 'auto' : (window.innerWidth - pos.right) + 'px';
|
|
||||||
|
|
||||||
r.tt.innerHTML = msg.replace(/\$N/g, "<br />");
|
r.tt.innerHTML = msg.replace(/\$N/g, "<br />");
|
||||||
|
r.el.addEventListener('mouseleave', r.hide);
|
||||||
|
window.addEventListener('scroll', r.hide);
|
||||||
clmod(r.tt, 'show', 1);
|
clmod(r.tt, 'show', 1);
|
||||||
|
|
||||||
|
var tw = r.tt.offsetWidth,
|
||||||
|
x = pos.left + (pos.right - pos.left) / 2 - tw / 2;
|
||||||
|
|
||||||
|
if (x + tw >= window.innerWidth - 24)
|
||||||
|
x = window.innerWidth - tw - 24;
|
||||||
|
|
||||||
|
if (x < 0)
|
||||||
|
x = 12;
|
||||||
|
|
||||||
|
r.tt.style.left = x + 'px';
|
||||||
|
r.tt.style.top = top ? (margin + pos.bottom) + 'px' : 'auto';
|
||||||
|
r.tt.style.bottom = top ? 'auto' : (margin + window.innerHeight - pos.top) + 'px';
|
||||||
|
};
|
||||||
|
|
||||||
|
r.hide = function (e) {
|
||||||
|
ev(e);
|
||||||
|
window.removeEventListener('scroll', r.hide);
|
||||||
|
clmod(r.tt, 'show');
|
||||||
|
if (r.el)
|
||||||
|
r.el.removeEventListener('mouseleave', r.hide);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (is_touch && IPHONE) {
|
||||||
|
var f1 = r.show,
|
||||||
|
f2 = r.hide,
|
||||||
|
q = [];
|
||||||
|
|
||||||
|
// if an onclick-handler creates a new timer,
|
||||||
|
// iOS 13.1.2 delays the entire handler by up to 401ms,
|
||||||
|
// win by using a shared timer instead
|
||||||
|
|
||||||
|
timer.add(function () {
|
||||||
|
while (q.length && Date.now() >= q[0][0])
|
||||||
|
q.shift()[1]();
|
||||||
|
});
|
||||||
|
|
||||||
|
r.show = function () {
|
||||||
|
q.push([Date.now() + 100, f1.bind(this)]);
|
||||||
|
};
|
||||||
|
r.hide = function () {
|
||||||
|
q.push([Date.now() + 100, f2.bind(this)]);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function hide() {
|
r.tt.onclick = r.hide;
|
||||||
clmod(r.tt, 'show');
|
|
||||||
|
r.att = function (ctr) {
|
||||||
|
var _show = r.en ? r.show : null,
|
||||||
|
_hide = r.en ? r.hide : null,
|
||||||
|
o = ctr.querySelectorAll('*[tt]');
|
||||||
|
|
||||||
|
for (var a = o.length - 1; a >= 0; a--) {
|
||||||
|
o[a].onfocus = _show;
|
||||||
|
o[a].onblur = _hide;
|
||||||
|
o[a].onmouseenter = _show;
|
||||||
|
o[a].onmouseleave = _hide;
|
||||||
|
}
|
||||||
|
r.hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
r.init = function () {
|
r.init = function () {
|
||||||
@@ -542,19 +785,322 @@ var tt = (function () {
|
|||||||
};
|
};
|
||||||
r.en = bcfg_get('tooltips', true)
|
r.en = bcfg_get('tooltips', true)
|
||||||
}
|
}
|
||||||
|
r.att(document);
|
||||||
var _show = r.en ? show : null,
|
|
||||||
_hide = r.en ? hide : null;
|
|
||||||
|
|
||||||
var o = QSA('*[tt]');
|
|
||||||
for (var a = o.length - 1; a >= 0; a--) {
|
|
||||||
o[a].onfocus = _show;
|
|
||||||
o[a].onblur = _hide;
|
|
||||||
o[a].onmouseenter = _show;
|
|
||||||
o[a].onmouseleave = _hide;
|
|
||||||
}
|
|
||||||
hide();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return r;
|
return r;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
||||||
|
function lf2br(txt) {
|
||||||
|
var html = '', hp = txt.split(/(?=<.?pre>)/i);
|
||||||
|
for (var a = 0; a < hp.length; a++)
|
||||||
|
html += hp[a].startsWith('<pre>') ? hp[a] :
|
||||||
|
hp[a].replace(/<br ?.?>\n/g, '\n').replace(/\n<br ?.?>/g, '\n').replace(/\n/g, '<br />\n');
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var toast = (function () {
|
||||||
|
var r = {},
|
||||||
|
te = null,
|
||||||
|
scrolling = false,
|
||||||
|
obj = mknod('div');
|
||||||
|
|
||||||
|
obj.setAttribute('id', 'toast');
|
||||||
|
document.body.appendChild(obj);
|
||||||
|
r.visible = false;
|
||||||
|
r.txt = null;
|
||||||
|
|
||||||
|
function scrollchk() {
|
||||||
|
if (scrolling)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var tb = ebi('toastb'),
|
||||||
|
vis = tb.offsetHeight,
|
||||||
|
all = tb.scrollHeight;
|
||||||
|
|
||||||
|
if (8 + vis >= all)
|
||||||
|
return;
|
||||||
|
|
||||||
|
clmod(obj, 'scroll', 1);
|
||||||
|
scrolling = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function unscroll() {
|
||||||
|
timer.rm(scrollchk);
|
||||||
|
clmod(obj, 'scroll');
|
||||||
|
scrolling = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
r.hide = function (e) {
|
||||||
|
ev(e);
|
||||||
|
unscroll();
|
||||||
|
clearTimeout(te);
|
||||||
|
clmod(obj, 'vis');
|
||||||
|
r.visible = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
r.show = function (cl, sec, txt) {
|
||||||
|
clearTimeout(te);
|
||||||
|
if (sec)
|
||||||
|
te = setTimeout(r.hide, sec * 1000);
|
||||||
|
|
||||||
|
obj.innerHTML = '<a href="#" id="toastc">x</a><div id="toastb">' + lf2br(txt) + '</div>';
|
||||||
|
obj.className = cl;
|
||||||
|
sec += obj.offsetWidth;
|
||||||
|
obj.className += ' vis';
|
||||||
|
ebi('toastc').onclick = r.hide;
|
||||||
|
timer.add(scrollchk);
|
||||||
|
r.visible = true;
|
||||||
|
r.txt = txt;
|
||||||
|
};
|
||||||
|
|
||||||
|
r.ok = function (sec, txt) {
|
||||||
|
r.show('ok', sec, txt);
|
||||||
|
};
|
||||||
|
r.inf = function (sec, txt) {
|
||||||
|
r.show('inf', sec, txt);
|
||||||
|
};
|
||||||
|
r.warn = function (sec, txt) {
|
||||||
|
r.show('warn', sec, txt);
|
||||||
|
};
|
||||||
|
r.err = function (sec, txt) {
|
||||||
|
r.show('err', sec, txt);
|
||||||
|
};
|
||||||
|
|
||||||
|
return r;
|
||||||
|
})();
|
||||||
|
|
||||||
|
|
||||||
|
var modal = (function () {
|
||||||
|
var r = {},
|
||||||
|
q = [],
|
||||||
|
o = null,
|
||||||
|
cb_up = null,
|
||||||
|
cb_ok = null,
|
||||||
|
cb_ng = null,
|
||||||
|
prim = '<a href="#" id="modal-ok">OK</a>',
|
||||||
|
sec = '<a href="#" id="modal-ng">Cancel</a>',
|
||||||
|
ok_cancel = WINDOWS ? prim + sec : sec + prim;
|
||||||
|
|
||||||
|
r.busy = false;
|
||||||
|
|
||||||
|
r.show = function (html) {
|
||||||
|
o = mknod('div');
|
||||||
|
o.setAttribute('id', 'modal');
|
||||||
|
o.innerHTML = '<table><tr><td><div id="modalc">' + html + '</div></td></tr></table>';
|
||||||
|
document.body.appendChild(o);
|
||||||
|
document.addEventListener('keydown', onkey);
|
||||||
|
r.busy = true;
|
||||||
|
|
||||||
|
var a = ebi('modal-ng');
|
||||||
|
if (a)
|
||||||
|
a.onclick = ng;
|
||||||
|
|
||||||
|
a = ebi('modal-ok');
|
||||||
|
a.onclick = ok;
|
||||||
|
|
||||||
|
var inp = ebi('modali');
|
||||||
|
(inp || a).focus();
|
||||||
|
if (inp)
|
||||||
|
setTimeout(function () {
|
||||||
|
inp.setSelectionRange(0, inp.value.length, "forward");
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
document.addEventListener('focus', onfocus);
|
||||||
|
timer.add(onfocus);
|
||||||
|
if (cb_up)
|
||||||
|
setTimeout(cb_up, 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
r.hide = function () {
|
||||||
|
timer.rm(onfocus);
|
||||||
|
document.removeEventListener('focus', onfocus);
|
||||||
|
document.removeEventListener('keydown', onkey);
|
||||||
|
o.parentNode.removeChild(o);
|
||||||
|
r.busy = false;
|
||||||
|
setTimeout(next, 50);
|
||||||
|
};
|
||||||
|
function ok(e) {
|
||||||
|
ev(e);
|
||||||
|
var v = ebi('modali');
|
||||||
|
v = v ? v.value : true;
|
||||||
|
r.hide();
|
||||||
|
if (cb_ok)
|
||||||
|
cb_ok(v);
|
||||||
|
}
|
||||||
|
function ng(e) {
|
||||||
|
ev(e);
|
||||||
|
r.hide();
|
||||||
|
if (cb_ng)
|
||||||
|
cb_ng(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onfocus(e) {
|
||||||
|
var ctr = ebi('modalc');
|
||||||
|
if (!ctr || !ctr.contains || !document.activeElement || ctr.contains(document.activeElement))
|
||||||
|
return;
|
||||||
|
|
||||||
|
setTimeout(function () {
|
||||||
|
ebi('modal-ok').focus();
|
||||||
|
}, 20);
|
||||||
|
ev(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onkey(e) {
|
||||||
|
if (e.code == 'Enter') {
|
||||||
|
var a = ebi('modal-ng');
|
||||||
|
if (a && document.activeElement == a)
|
||||||
|
return ng();
|
||||||
|
|
||||||
|
return ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.code == 'Escape')
|
||||||
|
return ng();
|
||||||
|
}
|
||||||
|
|
||||||
|
function next() {
|
||||||
|
if (!r.busy && q.length)
|
||||||
|
q.shift()();
|
||||||
|
}
|
||||||
|
|
||||||
|
r.alert = function (html, cb, fun) {
|
||||||
|
q.push(function () {
|
||||||
|
_alert(lf2br(html), cb, fun);
|
||||||
|
});
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
function _alert(html, cb, fun) {
|
||||||
|
cb_ok = cb_ng = cb;
|
||||||
|
cb_up = fun;
|
||||||
|
html += '<div id="modalb"><a href="#" id="modal-ok">OK</a></div>';
|
||||||
|
r.show(html);
|
||||||
|
}
|
||||||
|
|
||||||
|
r.confirm = function (html, cok, cng, fun) {
|
||||||
|
q.push(function () {
|
||||||
|
_confirm(lf2br(html), cok, cng, fun);
|
||||||
|
});
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
function _confirm(html, cok, cng, fun) {
|
||||||
|
cb_ok = cok;
|
||||||
|
cb_ng = cng === undefined ? cok : null;
|
||||||
|
cb_up = fun;
|
||||||
|
html += '<div id="modalb">' + ok_cancel + '</div>';
|
||||||
|
r.show(html);
|
||||||
|
}
|
||||||
|
|
||||||
|
r.prompt = function (html, v, cok, cng, fun) {
|
||||||
|
q.push(function () {
|
||||||
|
_prompt(lf2br(html), v, cok, cng, fun);
|
||||||
|
});
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
function _prompt(html, v, cok, cng, fun) {
|
||||||
|
cb_ok = cok;
|
||||||
|
cb_ng = cng === undefined ? cok : null;
|
||||||
|
cb_up = fun;
|
||||||
|
html += '<input id="modali" type="text" /><div id="modalb">' + ok_cancel + '</div>';
|
||||||
|
r.show(html);
|
||||||
|
|
||||||
|
ebi('modali').value = v || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return r;
|
||||||
|
})();
|
||||||
|
|
||||||
|
|
||||||
|
function winpopup(txt) {
|
||||||
|
fetch(get_evpath(), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
|
||||||
|
},
|
||||||
|
body: 'msg=' + uricom_enc(Date.now() + ', ' + txt)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var last_repl = null;
|
||||||
|
function repl_load() {
|
||||||
|
var ipre = ebi('repl_pre'),
|
||||||
|
tb = ebi('modali');
|
||||||
|
|
||||||
|
function getpres() {
|
||||||
|
var o, ret = jread("repl_pre", []);
|
||||||
|
if (!ret.length)
|
||||||
|
ret = [
|
||||||
|
'var v=Object.keys(localStorage); v.sort(); JSON.stringify(v)',
|
||||||
|
'console.hist.slice(-10).join("\\n")'
|
||||||
|
];
|
||||||
|
|
||||||
|
ipre.innerHTML = '<option value=""></option>';
|
||||||
|
for (var a = 0; a < ret.length; a++) {
|
||||||
|
o = mknod('option');
|
||||||
|
o.setAttribute('value', ret[a]);
|
||||||
|
o.textContent = ret[a];
|
||||||
|
ipre.appendChild(o);
|
||||||
|
}
|
||||||
|
last_repl = ipre.value = (last_repl || (ret.length ? ret.slice(-1)[0] : ''));
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
ebi('repl_pdel').onclick = function (e) {
|
||||||
|
var val = ipre.value,
|
||||||
|
pres = getpres();
|
||||||
|
|
||||||
|
apop(pres, val);
|
||||||
|
jwrite('repl_pre', pres);
|
||||||
|
getpres();
|
||||||
|
};
|
||||||
|
ebi('repl_pnew').onclick = function (e) {
|
||||||
|
var val = tb.value,
|
||||||
|
pres = getpres();
|
||||||
|
|
||||||
|
apop(pres, ipre.value);
|
||||||
|
pres.push(val);
|
||||||
|
jwrite('repl_pre', pres);
|
||||||
|
getpres();
|
||||||
|
ipre.value = val;
|
||||||
|
};
|
||||||
|
ipre.oninput = ipre.onchange = function () {
|
||||||
|
tb.value = last_repl = ipre.value;
|
||||||
|
};
|
||||||
|
tb.oninput = function () {
|
||||||
|
last_repl = this.value;
|
||||||
|
};
|
||||||
|
getpres();
|
||||||
|
tb.value = last_repl;
|
||||||
|
setTimeout(function () {
|
||||||
|
tb.setSelectionRange(0, tb.value.length, "forward");
|
||||||
|
}, 10);
|
||||||
|
}
|
||||||
|
function repl(e) {
|
||||||
|
ev(e);
|
||||||
|
var html = [
|
||||||
|
'<p>js repl (prefix with <code>,</code> to allow raise)</p>',
|
||||||
|
'<p><select id="repl_pre"></select>',
|
||||||
|
' <button id="repl_pdel">❌ del</button>',
|
||||||
|
' <button id="repl_pnew">💾 SAVE</button></p>'
|
||||||
|
];
|
||||||
|
|
||||||
|
modal.prompt(html.join(''), '', function (cmd) {
|
||||||
|
if (!cmd)
|
||||||
|
return toast.inf(3, 'eval aborted');
|
||||||
|
|
||||||
|
if (cmd.startsWith(','))
|
||||||
|
return modal.alert(esc(eval(cmd.slice(1)) + ''))
|
||||||
|
|
||||||
|
try {
|
||||||
|
modal.alert(esc(eval(cmd) + ''));
|
||||||
|
}
|
||||||
|
catch (ex) {
|
||||||
|
modal.alert('<h6>exception</h6>' + esc(ex + ''));
|
||||||
|
}
|
||||||
|
}, undefined, repl_load);
|
||||||
|
}
|
||||||
|
if (ebi('repl'))
|
||||||
|
ebi('repl').onclick = repl;
|
||||||
|
|||||||
@@ -1,11 +1,21 @@
|
|||||||
# example `.epilogue.html`
|
**NOTE:** there's more stuff (sharex config, service scripts, nginx configs, ...) in [`/contrib/`](/contrib/)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# example resource files
|
||||||
|
|
||||||
|
can be provided to copyparty to tweak things
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## example `.epilogue.html`
|
||||||
save one of these as `.epilogue.html` inside a folder to customize it:
|
save one of these as `.epilogue.html` inside a folder to customize it:
|
||||||
|
|
||||||
* [`minimal-up2k.html`](minimal-up2k.html) will [simplify the upload ui](https://user-images.githubusercontent.com/241032/118311195-dd6ca380-b4ef-11eb-86f3-75a3ff2e1332.png)
|
* [`minimal-up2k.html`](minimal-up2k.html) will [simplify the upload ui](https://user-images.githubusercontent.com/241032/118311195-dd6ca380-b4ef-11eb-86f3-75a3ff2e1332.png)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# example browser-css
|
## example browser-css
|
||||||
point `--css-browser` to one of these by URL:
|
point `--css-browser` to one of these by URL:
|
||||||
|
|
||||||
* [`browser.css`](browser.css) changes the background
|
* [`browser.css`](browser.css) changes the background
|
||||||
@@ -19,4 +29,23 @@ point `--css-browser` to one of these by URL:
|
|||||||
* notes on using rclone as a fuse client/server
|
* notes on using rclone as a fuse client/server
|
||||||
|
|
||||||
## [`example.conf`](example.conf)
|
## [`example.conf`](example.conf)
|
||||||
* example config file for `-c` which never really happened
|
* example config file for `-c` (supports accounts, volumes, and volume-flags)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# junk
|
||||||
|
|
||||||
|
alphabetical list of the remaining files
|
||||||
|
|
||||||
|
| what | why |
|
||||||
|
| -- | -- |
|
||||||
|
| [biquad.html](biquad.html) | bruteforce calibrator for the audio equalizer since im not that good at maths |
|
||||||
|
| [design.txt](design.txt) | initial brainstorming of the copyparty design, unmaintained, incorrect, sentimental value only |
|
||||||
|
| [hls.html](hls.html) | experimenting with hls playback using `hls.js`, works p well, almost became a thing |
|
||||||
|
| [music-analysis.sh](music-analysis.sh) | testing various bpm/key detection libraries before settling on the ones used in [`/bin/mtag/`](/bin/mtag/) |
|
||||||
|
| [notes.sh](notes.sh) | notepad, just scraps really |
|
||||||
|
| [nuitka.txt](nuitka.txt) | how to build a copyparty exe using nuitka (not maintained) |
|
||||||
|
| [pretend-youre-qnap.patch](pretend-youre-qnap.patch) | simulate a NAS which keeps returning old cached data even though you just modified the file yourself |
|
||||||
|
| [tcp-debug.sh](tcp-debug.sh) | looks like this was to debug stuck tcp connections? |
|
||||||
|
| [unirange.py](unirange.py) | uhh |
|
||||||
|
| [up2k.txt](up2k.txt) | initial ideas for how up2k should work, another unmaintained sentimental-value-only thing |
|
||||||
|
|||||||
@@ -1,37 +1,7 @@
|
|||||||
/* put filetype icons inline with text
|
/* video, alternative 1:
|
||||||
#ggrid>a>span:before,
|
top-left icon, just like the other formats
|
||||||
#ggrid>a>span.dir:before {
|
=======================================================================
|
||||||
display: inline;
|
|
||||||
line-height: 0;
|
|
||||||
font-size: 1.7em;
|
|
||||||
margin: -.7em .1em -.5em -.6em;
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
/* move folder icons top-left */
|
|
||||||
#ggrid>a>span.dir:before {
|
|
||||||
content: initial;
|
|
||||||
}
|
|
||||||
#ggrid>a[href$="/"]:before {
|
|
||||||
content: '📂';
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* put filetype icons top-left */
|
|
||||||
#ggrid>a:before {
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
padding: .3em 0;
|
|
||||||
margin: -.4em;
|
|
||||||
text-shadow: 0 0 .1em #000;
|
|
||||||
background: linear-gradient(135deg,rgba(255,255,255,0) 50%,rgba(255,255,255,0.2));
|
|
||||||
border-radius: .3em;
|
|
||||||
font-size: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* video */
|
|
||||||
#ggrid>a:is(
|
#ggrid>a:is(
|
||||||
[href$=".mkv"i],
|
[href$=".mkv"i],
|
||||||
[href$=".mp4"i],
|
[href$=".mp4"i],
|
||||||
@@ -39,6 +9,40 @@
|
|||||||
):before {
|
):before {
|
||||||
content: '📺';
|
content: '📺';
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* video, alternative 2:
|
||||||
|
play-icon in the middle of the thumbnail
|
||||||
|
=======================================================================
|
||||||
|
*/
|
||||||
|
#ggrid>a:is(
|
||||||
|
[href$=".mkv"i],
|
||||||
|
[href$=".mp4"i],
|
||||||
|
[href$=".webm"i],
|
||||||
|
) {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
#ggrid>a:is(
|
||||||
|
[href$=".mkv"i],
|
||||||
|
[href$=".mp4"i],
|
||||||
|
[href$=".webm"i],
|
||||||
|
):before {
|
||||||
|
content: '▶';
|
||||||
|
opacity: .8;
|
||||||
|
margin: 0;
|
||||||
|
padding: 1em .5em 1em .7em;
|
||||||
|
border-radius: 9em;
|
||||||
|
line-height: 0;
|
||||||
|
color: #fff;
|
||||||
|
text-shadow: none;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
left: calc(50% - 1em);
|
||||||
|
top: calc(50% - 1.4em);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* audio */
|
/* audio */
|
||||||
@@ -54,6 +58,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* image */
|
/* image */
|
||||||
#ggrid>a:is(
|
#ggrid>a:is(
|
||||||
[href$=".jpg"i],
|
[href$=".jpg"i],
|
||||||
|
|||||||
@@ -10,19 +10,25 @@ u k:k
|
|||||||
# share "." (the current directory)
|
# share "." (the current directory)
|
||||||
# as "/" (the webroot) for the following users:
|
# as "/" (the webroot) for the following users:
|
||||||
# "r" grants read-access for anyone
|
# "r" grants read-access for anyone
|
||||||
# "a ed" grants read-write to ed
|
# "rw ed" grants read-write to ed
|
||||||
.
|
.
|
||||||
/
|
/
|
||||||
r
|
r
|
||||||
a ed
|
rw ed
|
||||||
|
|
||||||
# custom permissions for the "priv" folder:
|
# custom permissions for the "priv" folder:
|
||||||
# user "k" can see/read the contents
|
# user "k" can only see/read the contents
|
||||||
# and "ed" gets read-write access
|
# user "ed" gets read-write access
|
||||||
./priv
|
./priv
|
||||||
/priv
|
/priv
|
||||||
r k
|
r k
|
||||||
a ed
|
rw ed
|
||||||
|
|
||||||
|
# this does the same thing:
|
||||||
|
./priv
|
||||||
|
/priv
|
||||||
|
r ed k
|
||||||
|
w ed
|
||||||
|
|
||||||
# share /home/ed/Music/ as /music and let anyone read it
|
# share /home/ed/Music/ as /music and let anyone read it
|
||||||
# (this will replace any folder called "music" in the webroot)
|
# (this will replace any folder called "music" in the webroot)
|
||||||
@@ -41,5 +47,5 @@ c e2d
|
|||||||
c nodupe
|
c nodupe
|
||||||
|
|
||||||
# this entire config file can be replaced with these arguments:
|
# this entire config file can be replaced with these arguments:
|
||||||
# -u ed:123 -u k:k -v .::r:aed -v priv:priv:rk:aed -v /home/ed/Music:music:r -v /home/ed/inc:dump:w
|
# -u ed:123 -u k:k -v .::r:a,ed -v priv:priv:r,k:rw,ed -v /home/ed/Music:music:r -v /home/ed/inc:dump:w:c,e2d:c,nodupe
|
||||||
# but note that the config file always wins in case of conflicts
|
# but note that the config file always wins in case of conflicts
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
#u2cleanup, #u2conf tr:first-child>td[rowspan]:not(#u2btn_cw), /* most of the config options */
|
#u2cleanup, #u2conf tr:first-child>td[rowspan]:not(#u2btn_cw), /* most of the config options */
|
||||||
|
|
||||||
#u2cards /* and the upload progress tabs */
|
#u2cards, #u2etaw /* and the upload progress tabs */
|
||||||
|
|
||||||
{display: none !important} /* do it! */
|
{display: none !important} /* do it! */
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
/* add some margins because now it's weird */
|
/* add some margins because now it's weird */
|
||||||
.opview {margin-top: 2.5em}
|
.opview {margin-top: 2.5em}
|
||||||
#op_up2k {margin-top: 3em}
|
#op_up2k {margin-top: 6em}
|
||||||
|
|
||||||
/* and embiggen the upload button */
|
/* and embiggen the upload button */
|
||||||
#u2conf #u2btn, #u2btn {padding:1.5em 0}
|
#u2conf #u2btn, #u2btn {padding:1.5em 0}
|
||||||
@@ -27,6 +27,9 @@
|
|||||||
/* adjust the button area a bit */
|
/* adjust the button area a bit */
|
||||||
#u2conf.has_btn {width: 35em !important; margin: 5em auto}
|
#u2conf.has_btn {width: 35em !important; margin: 5em auto}
|
||||||
|
|
||||||
|
/* a */
|
||||||
|
#op_up2k {min-height: 0}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<a href="#" onclick="this.parentNode.innerHTML='';">show advanced options</a>
|
<a href="#" onclick="this.parentNode.innerHTML='';">show advanced options</a>
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
|
|
||||||
method = self.s.recv(4)
|
|
||||||
self.s.unrecv(method)
|
|
||||||
print("xxx unrecv'd [{}]".format(method))
|
|
||||||
|
|
||||||
# jython used to do this, they stopped since it's broken
|
|
||||||
# but reimplementing sendall is out of scope for now
|
|
||||||
if not getattr(self.s.s, "sendall", None):
|
|
||||||
self.s.s.sendall = self.s.s.send
|
|
||||||
|
|
||||||
# TODO this is also pretty bad
|
|
||||||
have = dir(self.s)
|
|
||||||
for k in self.s.s.__dict__:
|
|
||||||
if k not in have and not k.startswith("__"):
|
|
||||||
if k == "recv":
|
|
||||||
raise Exception("wait what")
|
|
||||||
|
|
||||||
self.s.__dict__[k] = self.s.s.__dict__[k]
|
|
||||||
|
|
||||||
have = dir(self.s)
|
|
||||||
for k in dir(self.s.s):
|
|
||||||
if k not in have and not k.startswith("__"):
|
|
||||||
if k == "recv":
|
|
||||||
raise Exception("wait what")
|
|
||||||
|
|
||||||
setattr(self.s, k, getattr(self.s.s, k))
|
|
||||||
@@ -44,7 +44,7 @@ avg() { awk 'function pr(ncsz) {if (nsmp>0) {printf "%3s %s\n", csz, sum/nsmp} c
|
|||||||
dirs=("$HOME/vfs/ほげ" "$HOME/vfs/ほげ/ぴよ" "$HOME/vfs/$(printf \\xed\\x91)" "$HOME/vfs/$(printf \\xed\\x91/\\xed\\x92)")
|
dirs=("$HOME/vfs/ほげ" "$HOME/vfs/ほげ/ぴよ" "$HOME/vfs/$(printf \\xed\\x91)" "$HOME/vfs/$(printf \\xed\\x91/\\xed\\x92)")
|
||||||
mkdir -p "${dirs[@]}"
|
mkdir -p "${dirs[@]}"
|
||||||
for dir in "${dirs[@]}"; do for fn in ふが "$(printf \\xed\\x93)" 'qwe,rty;asd fgh+jkl%zxc&vbn <qwe>"rty'"'"'uio&asd fgh'; do echo "$dir" > "$dir/$fn.html"; done; done
|
for dir in "${dirs[@]}"; do for fn in ふが "$(printf \\xed\\x93)" 'qwe,rty;asd fgh+jkl%zxc&vbn <qwe>"rty'"'"'uio&asd fgh'; do echo "$dir" > "$dir/$fn.html"; done; done
|
||||||
|
# qw er+ty%20ui%%20op<as>df&gh&jk#zx'cv"bn`m=qw*er^ty?ui@op,as.df-gh_jk
|
||||||
|
|
||||||
##
|
##
|
||||||
## upload mojibake
|
## upload mojibake
|
||||||
@@ -79,6 +79,10 @@ command -v gdate && date() { gdate "$@"; }; while true; do t=$(date +%s.%N); (ti
|
|||||||
# get all up2k search result URLs
|
# get all up2k search result URLs
|
||||||
var t=[]; var b=document.location.href.split('#')[0].slice(0, -1); document.querySelectorAll('#u2tab .prog a').forEach((x) => {t.push(b+encodeURI(x.getAttribute("href")))}); console.log(t.join("\n"));
|
var t=[]; var b=document.location.href.split('#')[0].slice(0, -1); document.querySelectorAll('#u2tab .prog a').forEach((x) => {t.push(b+encodeURI(x.getAttribute("href")))}); console.log(t.join("\n"));
|
||||||
|
|
||||||
|
# rename all selected songs to <leading-track-number> + <Title> + <extension>
|
||||||
|
var sel=msel.getsel(), ci=find_file_col('Title')[0], re=[]; for (var a=0; a<sel.length; a++) { var url=sel[a].vp, tag=ebi(sel[a].id).closest('tr').querySelectorAll('td')[ci].textContent, name=uricom_dec(vsplit(url)[1])[0], m=/^([0-9]+[\. -]+)?.*(\.[^\.]+$)/.exec(name), name2=(m[1]||'')+tag+m[2], url2=vsplit(url)[0]+uricom_enc(name2,false); if (url!=url2) re.push([url, url2]); }
|
||||||
|
console.log(JSON.stringify(re, null, ' '));
|
||||||
|
function f() { if (!re.length) return treectl.goto(get_evpath()); var [u1,u2] = re.shift(); fetch(u1+'?move='+u2).then((rsp) => {if (rsp.ok) f(); }); }; f();
|
||||||
|
|
||||||
##
|
##
|
||||||
## bash oneliners
|
## bash oneliners
|
||||||
@@ -122,6 +126,13 @@ e=6; s=10; d=~/dev/copyparty/srv/aus; n=1; p=0; e=$((e*60)); rm -rf $d; mkdir $d
|
|||||||
-v srv/aus:aus:r:ce2dsa:ce2ts:cmtp=fgsfds=bin/mtag/sleep.py
|
-v srv/aus:aus:r:ce2dsa:ce2ts:cmtp=fgsfds=bin/mtag/sleep.py
|
||||||
sqlite3 .hist/up2k.db 'select * from mt where k="fgsfds" or k="t:mtp"' | tee /dev/stderr | wc -l
|
sqlite3 .hist/up2k.db 'select * from mt where k="fgsfds" or k="t:mtp"' | tee /dev/stderr | wc -l
|
||||||
|
|
||||||
|
# generate the sine meme
|
||||||
|
for ((f=420;f<1200;f++)); do sz=$(ffmpeg -y -f lavfi -i sine=frequency=$f:duration=2 -vf volume=0.1 -ac 1 -ar 44100 -f s16le /dev/shm/a.wav 2>/dev/null; base64 -w0 </dev/shm/a.wav | gzip -c | wc -c); printf '%d %d\n' $f $sz; done | tee /dev/stderr | sort -nrk2,2
|
||||||
|
ffmpeg -y -f lavfi -i sine=frequency=1050:duration=2 -vf volume=0.1 -ac 1 -ar 44100 /dev/shm/a.wav
|
||||||
|
|
||||||
|
# play icon calibration pics
|
||||||
|
for w in 150 170 190 210 230 250; do for h in 130 150 170 190 210; do /c/Program\ Files/ImageMagick-7.0.11-Q16-HDRI/magick.exe convert -size ${w}x${h} xc:brown -fill orange -draw "circle $((w/2)),$((h/2)) $((w/2)),$((h/3))" $w-$h.png; done; done
|
||||||
|
|
||||||
|
|
||||||
##
|
##
|
||||||
## vscode
|
## vscode
|
||||||
@@ -153,7 +164,7 @@ brew install python@2
|
|||||||
pip install virtualenv
|
pip install virtualenv
|
||||||
|
|
||||||
# readme toc
|
# readme toc
|
||||||
cat README.md | awk '!/^#/{next} {lv=length($1);sub(/[^ ]+ /,"");bab=$0;gsub(/ /,"-",bab)} {printf "%" ((lv-1)*4+1) "s [%s](#%s)\n", "*",$0,bab}'
|
cat README.md | awk 'function pr() { if (!h) {return}; if (/^ *[*!#]/||!s) {printf "%s\n",h;h=0;return}; if (/.../) {printf "%s - %s\n",h,$0;h=0}; }; /^#/{s=1;pr()} /^#* *(file indexing|install on android|dev env setup|just the sfx|complete release|optional gpl stuff)|`$/{s=0} /^#/{lv=length($1);sub(/[^ ]+ /,"");bab=$0;gsub(/ /,"-",bab); h=sprintf("%" ((lv-1)*4+1) "s [%s](#%s)", "*",$0,bab);next} !h{next} {sub(/ .*/,"");sub(/[:,]$/,"")} {pr()}' > toc; grep -E '^## readme toc' -B1000 -A2 <README.md >p1; grep -E '^## quickstart' -B2 -A999999 <README.md >p2; (cat p1; grep quickstart -A1000 <toc; cat p2) >README.md
|
||||||
|
|
||||||
# fix firefox phantom breakpoints,
|
# fix firefox phantom breakpoints,
|
||||||
# suggestions from bugtracker, doesnt work (debugger is not attachable)
|
# suggestions from bugtracker, doesnt work (debugger is not attachable)
|
||||||
@@ -166,7 +177,10 @@ dbg.asyncStore.pendingBreakpoints = {}
|
|||||||
about:config >> devtools.debugger.prefs-schema-version = -1
|
about:config >> devtools.debugger.prefs-schema-version = -1
|
||||||
|
|
||||||
# determine server version
|
# determine server version
|
||||||
git pull; git reset --hard origin/HEAD && git log --format=format:"%H %ai %d" --decorate=full > ../revs && cat ../{util,browser}.js >../vr && cat ../revs | while read -r rev extra; do (git reset --hard $rev >/dev/null 2>/dev/null && dsz=$(cat copyparty/web/{util,browser}.js >../vg 2>/dev/null && diff -wNarU0 ../{vg,vr} | wc -c) && printf '%s %6s %s\n' "$rev" $dsz "$extra") </dev/null; done
|
git pull; git reset --hard origin/HEAD && git log --format=format:"%H %ai %d" --decorate=full > ../revs && cat ../{util,browser,up2k}.js >../vr && cat ../revs | while read -r rev extra; do (git reset --hard $rev >/dev/null 2>/dev/null && dsz=$(cat copyparty/web/{util,browser,up2k}.js >../vg 2>/dev/null && diff -wNarU0 ../{vg,vr} | wc -c) && printf '%s %6s %s\n' "$rev" $dsz "$extra") </dev/null; done
|
||||||
|
|
||||||
|
# download all sfx versions
|
||||||
|
curl https://api.github.com/repos/9001/copyparty/releases?per_page=100 | jq -r '.[] | .tag_name + " " + .name' | tr -d '\r' | while read v t; do fn="copyparty $v $t.py"; [ -e "$fn" ] || curl https://github.com/9001/copyparty/releases/download/$v/copyparty-sfx.py -Lo "$fn"; done
|
||||||
|
|
||||||
|
|
||||||
##
|
##
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ WORKDIR /z
|
|||||||
ENV ver_asmcrypto=5b994303a9d3e27e0915f72a10b6c2c51535a4dc \
|
ENV ver_asmcrypto=5b994303a9d3e27e0915f72a10b6c2c51535a4dc \
|
||||||
ver_hashwasm=4.7.0 \
|
ver_hashwasm=4.7.0 \
|
||||||
ver_marked=1.1.0 \
|
ver_marked=1.1.0 \
|
||||||
ver_ogvjs=1.8.0 \
|
ver_ogvjs=1.8.4 \
|
||||||
ver_mde=2.14.0 \
|
ver_mde=2.14.0 \
|
||||||
ver_codemirror=5.59.3 \
|
ver_codemirror=5.59.3 \
|
||||||
ver_fontawesome=5.13.0 \
|
ver_fontawesome=5.13.0 \
|
||||||
@@ -74,23 +74,16 @@ RUN cd hash-wasm \
|
|||||||
# build ogvjs
|
# build ogvjs
|
||||||
RUN cd ogvjs-$ver_ogvjs \
|
RUN cd ogvjs-$ver_ogvjs \
|
||||||
&& cp -pv \
|
&& cp -pv \
|
||||||
ogv.js \
|
|
||||||
ogv-worker-audio.js \
|
ogv-worker-audio.js \
|
||||||
ogv-demuxer-ogg-wasm.js \
|
ogv-demuxer-ogg-wasm.js \
|
||||||
ogv-demuxer-ogg-wasm.wasm \
|
ogv-demuxer-ogg-wasm.wasm \
|
||||||
ogv-demuxer-webm-wasm.js \
|
|
||||||
ogv-demuxer-webm-wasm.wasm \
|
|
||||||
ogv-decoder-audio-opus-wasm.js \
|
ogv-decoder-audio-opus-wasm.js \
|
||||||
ogv-decoder-audio-opus-wasm.wasm \
|
ogv-decoder-audio-opus-wasm.wasm \
|
||||||
ogv-decoder-audio-vorbis-wasm.js \
|
ogv-decoder-audio-vorbis-wasm.js \
|
||||||
ogv-decoder-audio-vorbis-wasm.wasm \
|
ogv-decoder-audio-vorbis-wasm.wasm \
|
||||||
/z/dist
|
/z/dist \
|
||||||
|
&& cp -pv \
|
||||||
# ogv-demuxer-ogg.js \
|
ogv-es2017.js /z/dist/ogv.js
|
||||||
# ogv-demuxer-webm.js \
|
|
||||||
# ogv-decoder-audio-opus.js \
|
|
||||||
# ogv-decoder-audio-vorbis.js \
|
|
||||||
# dynamicaudio.swf \
|
|
||||||
|
|
||||||
|
|
||||||
# build marked
|
# build marked
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ all: $(addsuffix .gz, $(wildcard *.*))
|
|||||||
|
|
||||||
%.gz: %
|
%.gz: %
|
||||||
#brotli -q 11 $<
|
#brotli -q 11 $<
|
||||||
pigz -11 -J 34 -I 573 $<
|
pigz -11 -I 573 $<
|
||||||
|
|
||||||
# pigz -11 -J 34 -I 100 -F < $< > $@.first
|
# pigz -11 -J 34 -I 100 -F < $< > $@.first
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
set -e
|
set -e
|
||||||
echo
|
echo
|
||||||
|
|
||||||
|
help() { exec cat <<'EOF'
|
||||||
|
|
||||||
# optional args:
|
# optional args:
|
||||||
#
|
#
|
||||||
@@ -15,12 +16,19 @@ echo
|
|||||||
#
|
#
|
||||||
# `no-sh` makes just the python sfx, skips the sh/unix sfx
|
# `no-sh` makes just the python sfx, skips the sh/unix sfx
|
||||||
#
|
#
|
||||||
# `no-ogv` saves ~500k by removing the opus/vorbis audio codecs
|
# `no-ogv` saves ~192k by removing the opus/vorbis audio codecs
|
||||||
# (only affects apple devices; everything else has native support)
|
# (only affects apple devices; everything else has native support)
|
||||||
#
|
#
|
||||||
# `no-cm` saves ~90k by removing easymde/codemirror
|
# `no-cm` saves ~92k by removing easymde/codemirror
|
||||||
# (the fancy markdown editor)
|
# (the fancy markdown editor)
|
||||||
|
#
|
||||||
|
# `no-fnt` saves ~9k by removing the source-code-pro font
|
||||||
|
# (browsers will try to use 'Consolas' instead)
|
||||||
|
#
|
||||||
|
# `no-dd` saves ~2k by removing the mouse cursor
|
||||||
|
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
# port install gnutar findutils gsed coreutils
|
# port install gnutar findutils gsed coreutils
|
||||||
gtar=$(command -v gtar || command -v gnutar) || true
|
gtar=$(command -v gtar || command -v gnutar) || true
|
||||||
@@ -29,6 +37,9 @@ gtar=$(command -v gtar || command -v gnutar) || true
|
|||||||
sed() { gsed "$@"; }
|
sed() { gsed "$@"; }
|
||||||
find() { gfind "$@"; }
|
find() { gfind "$@"; }
|
||||||
sort() { gsort "$@"; }
|
sort() { gsort "$@"; }
|
||||||
|
shuf() { gshuf "$@"; }
|
||||||
|
nproc() { gnproc; }
|
||||||
|
sha1sum() { shasum "$@"; }
|
||||||
unexpand() { gunexpand "$@"; }
|
unexpand() { gunexpand "$@"; }
|
||||||
command -v grealpath >/dev/null &&
|
command -v grealpath >/dev/null &&
|
||||||
realpath() { grealpath "$@"; }
|
realpath() { grealpath "$@"; }
|
||||||
@@ -56,15 +67,22 @@ pybin=$(command -v python3 || command -v python) || {
|
|||||||
use_gz=
|
use_gz=
|
||||||
do_sh=1
|
do_sh=1
|
||||||
do_py=1
|
do_py=1
|
||||||
|
zopf=2560
|
||||||
while [ ! -z "$1" ]; do
|
while [ ! -z "$1" ]; do
|
||||||
[ "$1" = clean ] && clean=1 && shift && continue
|
case $1 in
|
||||||
[ "$1" = re ] && repack=1 && shift && continue
|
clean) clean=1 ; ;;
|
||||||
[ "$1" = gz ] && use_gz=1 && shift && continue
|
re) repack=1 ; ;;
|
||||||
[ "$1" = no-ogv ] && no_ogv=1 && shift && continue
|
gz) use_gz=1 ; ;;
|
||||||
[ "$1" = no-cm ] && no_cm=1 && shift && continue
|
no-ogv) no_ogv=1 ; ;;
|
||||||
[ "$1" = no-sh ] && do_sh= && shift && continue
|
no-fnt) no_fnt=1 ; ;;
|
||||||
[ "$1" = no-py ] && do_py= && shift && continue
|
no-dd) no_dd=1 ; ;;
|
||||||
break
|
no-cm) no_cm=1 ; ;;
|
||||||
|
no-sh) do_sh= ; ;;
|
||||||
|
no-py) do_py= ; ;;
|
||||||
|
fast) zopf=100 ; ;;
|
||||||
|
*) help ; ;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
done
|
done
|
||||||
|
|
||||||
tmv() {
|
tmv() {
|
||||||
@@ -72,16 +90,23 @@ tmv() {
|
|||||||
mv t "$1"
|
mv t "$1"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stamp=$(
|
||||||
|
for d in copyparty scripts; do
|
||||||
|
find $d -type f -printf '%TY-%Tm-%Td %TH:%TM:%TS %p\n'
|
||||||
|
done | sort | tail -n 1 | sha1sum | cut -c-16
|
||||||
|
)
|
||||||
|
|
||||||
rm -rf sfx/*
|
rm -rf sfx/*
|
||||||
mkdir -p sfx build
|
mkdir -p sfx build
|
||||||
cd sfx
|
cd sfx
|
||||||
|
|
||||||
[ $repack ] && {
|
tmpdir="$(
|
||||||
old="$(
|
|
||||||
printf '%s\n' "$TMPDIR" /tmp |
|
printf '%s\n' "$TMPDIR" /tmp |
|
||||||
awk '/./ {print; exit}'
|
awk '/./ {print; exit}'
|
||||||
)/pe-copyparty"
|
)"
|
||||||
|
|
||||||
|
[ $repack ] && {
|
||||||
|
old="$tmpdir/pe-copyparty"
|
||||||
echo "repack of files in $old"
|
echo "repack of files in $old"
|
||||||
cp -pR "$old/"*{dep-j2,copyparty} .
|
cp -pR "$old/"*{dep-j2,copyparty} .
|
||||||
}
|
}
|
||||||
@@ -113,7 +138,7 @@ cd sfx
|
|||||||
# msys2 tar is bad, make the best of it
|
# msys2 tar is bad, make the best of it
|
||||||
echo collecting source
|
echo collecting source
|
||||||
[ $clean ] && {
|
[ $clean ] && {
|
||||||
(cd .. && git archive master >tar) && tar -xf ../tar copyparty
|
(cd .. && git archive hovudstraum >tar) && tar -xf ../tar copyparty
|
||||||
(cd .. && tar -cf tar copyparty/web/deps) && tar -xf ../tar
|
(cd .. && tar -cf tar copyparty/web/deps) && tar -xf ../tar
|
||||||
}
|
}
|
||||||
[ $clean ] || {
|
[ $clean ] || {
|
||||||
@@ -123,6 +148,7 @@ cd sfx
|
|||||||
}
|
}
|
||||||
|
|
||||||
ver=
|
ver=
|
||||||
|
[ -z "$repack" ] &&
|
||||||
git describe --tags >/dev/null 2>/dev/null && {
|
git describe --tags >/dev/null 2>/dev/null && {
|
||||||
git_ver="$(git describe --tags)"; # v0.5.5-2-gb164aa0
|
git_ver="$(git describe --tags)"; # v0.5.5-2-gb164aa0
|
||||||
ver="$(printf '%s\n' "$git_ver" | sed -r 's/^v//')";
|
ver="$(printf '%s\n' "$git_ver" | sed -r 's/^v//')";
|
||||||
@@ -154,7 +180,7 @@ git describe --tags >/dev/null 2>/dev/null && {
|
|||||||
|
|
||||||
[ -z "$ver" ] &&
|
[ -z "$ver" ] &&
|
||||||
ver="$(awk '/^VERSION *= \(/ {
|
ver="$(awk '/^VERSION *= \(/ {
|
||||||
gsub(/[^0-9,]/,""); gsub(/,/,"."); print; exit}' < copyparty/__version__.py)"
|
gsub(/[^0-9,a-g-]/,""); gsub(/,/,"."); print; exit}' < copyparty/__version__.py)"
|
||||||
|
|
||||||
ts=$(date -u +%s)
|
ts=$(date -u +%s)
|
||||||
hts=$(date -u +%Y-%m%d-%H%M%S) # --date=@$ts (thx osx)
|
hts=$(date -u +%Y-%m%d-%H%M%S) # --date=@$ts (thx osx)
|
||||||
@@ -163,12 +189,12 @@ mkdir -p ../dist
|
|||||||
sfx_out=../dist/copyparty-sfx
|
sfx_out=../dist/copyparty-sfx
|
||||||
|
|
||||||
echo cleanup
|
echo cleanup
|
||||||
find .. -name '*.pyc' -delete
|
find -name '*.pyc' -delete
|
||||||
find .. -name __pycache__ -delete
|
find -name __pycache__ -delete
|
||||||
|
|
||||||
# especially prevent osx from leaking your lan ip (wtf apple)
|
# especially prevent osx from leaking your lan ip (wtf apple)
|
||||||
find .. -type f \( -name .DS_Store -or -name ._.DS_Store \) -delete
|
find -type f \( -name .DS_Store -or -name ._.DS_Store \) -delete
|
||||||
find .. -type f -name ._\* | while IFS= read -r f; do cmp <(printf '\x00\x05\x16') <(head -c 3 -- "$f") && rm -f -- "$f"; done
|
find -type f -name ._\* | while IFS= read -r f; do cmp <(printf '\x00\x05\x16') <(head -c 3 -- "$f") && rm -f -- "$f"; done
|
||||||
|
|
||||||
echo use smol web deps
|
echo use smol web deps
|
||||||
rm -f copyparty/web/deps/*.full.* copyparty/web/dbg-* copyparty/web/Makefile
|
rm -f copyparty/web/deps/*.full.* copyparty/web/dbg-* copyparty/web/Makefile
|
||||||
@@ -187,7 +213,24 @@ done
|
|||||||
rm -rf copyparty/web/mde.* copyparty/web/deps/easymde*
|
rm -rf copyparty/web/mde.* copyparty/web/deps/easymde*
|
||||||
echo h > copyparty/web/mde.html
|
echo h > copyparty/web/mde.html
|
||||||
f=copyparty/web/md.html
|
f=copyparty/web/md.html
|
||||||
sed -r '/edit2">edit \(fancy/d' <$f >t && tmv "$f"
|
sed -r '/edit2">edit \(fancy/d' <$f >t
|
||||||
|
tmv "$f"
|
||||||
|
}
|
||||||
|
|
||||||
|
[ $no_fnt ] && {
|
||||||
|
rm -f copyparty/web/deps/scp.woff2
|
||||||
|
f=copyparty/web/ui.css
|
||||||
|
gzip -d "$f.gz" || true
|
||||||
|
sed -r "s/src:.*scp.*\)/src:local('Consolas')/" <$f >t
|
||||||
|
tmv "$f"
|
||||||
|
}
|
||||||
|
|
||||||
|
[ $no_dd ] && {
|
||||||
|
rm -rf copyparty/web/dd
|
||||||
|
f=copyparty/web/browser.css
|
||||||
|
gzip -d "$f.gz" || true
|
||||||
|
sed -r 's/(cursor: ?)url\([^)]+\), ?(pointer)/\1\2/; /[0-9]+% \{cursor:/d; /animation: ?cursor/d' <$f >t
|
||||||
|
tmv "$f"
|
||||||
}
|
}
|
||||||
|
|
||||||
[ $repack ] ||
|
[ $repack ] ||
|
||||||
@@ -200,8 +243,15 @@ f=dep-j2/jinja2/constants.py
|
|||||||
awk '/^LOREM_IPSUM_WORDS/{o=1;print "LOREM_IPSUM_WORDS = u\"a\"";next} !o; /"""/{o=0}' <$f >t
|
awk '/^LOREM_IPSUM_WORDS/{o=1;print "LOREM_IPSUM_WORDS = u\"a\"";next} !o; /"""/{o=0}' <$f >t
|
||||||
tmv "$f"
|
tmv "$f"
|
||||||
|
|
||||||
|
grep -rLE '^#[^a-z]*coding: utf-8' dep-j2 |
|
||||||
|
while IFS= read -r f; do
|
||||||
|
(echo "# coding: utf-8"; cat "$f") >t
|
||||||
|
tmv "$f"
|
||||||
|
done
|
||||||
|
|
||||||
# up2k goes from 28k to 22k laff
|
# up2k goes from 28k to 22k laff
|
||||||
echo entabbening
|
awk 'BEGIN{gensub(//,"",1)}' </dev/null &&
|
||||||
|
echo entabbening &&
|
||||||
find | grep -E '\.css$' | while IFS= read -r f; do
|
find | grep -E '\.css$' | while IFS= read -r f; do
|
||||||
awk '{
|
awk '{
|
||||||
sub(/^[ \t]+/,"");
|
sub(/^[ \t]+/,"");
|
||||||
@@ -215,25 +265,61 @@ find | grep -E '\.css$' | while IFS= read -r f; do
|
|||||||
' <$f | sed 's/;\}$/}/' >t
|
' <$f | sed 's/;\}$/}/' >t
|
||||||
tmv "$f"
|
tmv "$f"
|
||||||
done
|
done
|
||||||
|
unexpand -h 2>/dev/null &&
|
||||||
find | grep -E '\.(js|html)$' | while IFS= read -r f; do
|
find | grep -E '\.(js|html)$' | while IFS= read -r f; do
|
||||||
unexpand -t 4 --first-only <"$f" >t
|
unexpand -t 4 --first-only <"$f" >t
|
||||||
tmv "$f"
|
tmv "$f"
|
||||||
done
|
done
|
||||||
|
|
||||||
|
|
||||||
gzres() {
|
gzres() {
|
||||||
command -v pigz &&
|
command -v pigz &&
|
||||||
pk='pigz -11 -J 34 -I 100' ||
|
pk="pigz -11 -I $zopf" ||
|
||||||
pk='gzip'
|
pk='gzip'
|
||||||
|
|
||||||
echo "$pk"
|
np=$(nproc)
|
||||||
find | grep -E '\.(js|css)$' | grep -vF /deps/ | while IFS= read -r f; do
|
echo "$pk #$np"
|
||||||
echo -n .
|
|
||||||
$pk "$f"
|
while IFS=' ' read -r _ f; do
|
||||||
|
while true; do
|
||||||
|
na=$(ps auxwww | grep -F "$pk" | wc -l)
|
||||||
|
[ $na -le $np ] && break
|
||||||
|
sleep 0.2
|
||||||
done
|
done
|
||||||
|
echo -n .
|
||||||
|
$pk "$f" &
|
||||||
|
done < <(
|
||||||
|
find -printf '%s %p\n' |
|
||||||
|
grep -E '\.(js|css)$' |
|
||||||
|
grep -vF /deps/ |
|
||||||
|
sort -nr
|
||||||
|
)
|
||||||
|
wait
|
||||||
echo
|
echo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
zdir="$tmpdir/cpp-mksfx"
|
||||||
|
[ -e "$zdir/$stamp" ] || rm -rf "$zdir"
|
||||||
|
mkdir -p "$zdir"
|
||||||
|
echo a > "$zdir/$stamp"
|
||||||
|
nf=$(ls -1 "$zdir"/arc.* | wc -l)
|
||||||
|
[ $nf -ge 2 ] && [ ! $repack ] && use_zdir=1 || use_zdir=
|
||||||
|
|
||||||
|
[ $use_zdir ] || {
|
||||||
|
echo "$nf alts += 1"
|
||||||
gzres
|
gzres
|
||||||
|
[ $repack ] ||
|
||||||
|
tar -cf "$zdir/arc.$(date +%s)" copyparty/web/*.gz
|
||||||
|
}
|
||||||
|
[ $use_zdir ] && {
|
||||||
|
arcs=("$zdir"/arc.*)
|
||||||
|
arc="${arcs[$RANDOM % ${#arcs[@]} ] }"
|
||||||
|
echo "using $arc"
|
||||||
|
tar -xf "$arc"
|
||||||
|
for f in copyparty/web/*.gz; do
|
||||||
|
rm "${f%.*}"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
echo gen tarlist
|
echo gen tarlist
|
||||||
@@ -241,7 +327,7 @@ for d in copyparty dep-j2; do find $d -type f; done |
|
|||||||
sed -r 's/(.*)\.(.*)/\2 \1/' | LC_ALL=C sort |
|
sed -r 's/(.*)\.(.*)/\2 \1/' | LC_ALL=C sort |
|
||||||
sed -r 's/([^ ]*) (.*)/\2.\1/' | grep -vE '/list1?$' > list1
|
sed -r 's/([^ ]*) (.*)/\2.\1/' | grep -vE '/list1?$' > list1
|
||||||
|
|
||||||
(grep -vE '\.(gz|br)$' list1; grep -E '\.(gz|br)$' list1) >list || true
|
(grep -vE '\.(gz|br)$' list1; grep -E '\.(gz|br)$' list1 | shuf) >list || true
|
||||||
|
|
||||||
echo creating tar
|
echo creating tar
|
||||||
args=(--owner=1000 --group=1000)
|
args=(--owner=1000 --group=1000)
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ rls_dir="$tmp/copyparty-$ver"
|
|||||||
mkdir "$rls_dir"
|
mkdir "$rls_dir"
|
||||||
|
|
||||||
echo ">>> export from git"
|
echo ">>> export from git"
|
||||||
git archive master | tar -xC "$rls_dir"
|
git archive hovudstraum | tar -xC "$rls_dir"
|
||||||
|
|
||||||
echo ">>> export untracked deps"
|
echo ">>> export untracked deps"
|
||||||
tar -c copyparty/web/deps | tar -xC "$rls_dir"
|
tar -c copyparty/web/deps | tar -xC "$rls_dir"
|
||||||
@@ -122,5 +122,5 @@ echo " $zip_path"
|
|||||||
echo " $tgz_path"
|
echo " $tgz_path"
|
||||||
echo
|
echo
|
||||||
|
|
||||||
# function alr() { ls -alR copyparty-$1 | sed -r "s/copyparty-$1/copyparty/" | sed -r 's/[A-Z][a-z]{2} [0-9 ]{2} [0-9]{2}:[0-9]{2}//' > $1; }; for x in master rls src ; do alr $x; done
|
# function alr() { ls -alR copyparty-$1 | sed -r "s/copyparty-$1/copyparty/" | sed -r 's/[A-Z][a-z]{2} [0-9 ]{2} [0-9]{2}:[0-9]{2}//' > $1; }; for x in hovudstraum rls src ; do alr $x; done
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import re, os, sys, time, shutil, signal, threading, tarfile, hashlib, platform,
|
|||||||
import subprocess as sp
|
import subprocess as sp
|
||||||
|
|
||||||
"""
|
"""
|
||||||
pls don't edit this file with a text editor,
|
to edit this file, use HxD or "vim -b"
|
||||||
it breaks the compressed stuff at the end
|
(there is compressed stuff at the end)
|
||||||
|
|
||||||
run me with any version of python, i will unpack and run copyparty
|
run me with any version of python, i will unpack and run copyparty
|
||||||
|
|
||||||
@@ -364,7 +364,7 @@ def confirm(rv):
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
sys.exit(rv)
|
sys.exit(rv or 1)
|
||||||
|
|
||||||
|
|
||||||
def run(tmp, j2):
|
def run(tmp, j2):
|
||||||
@@ -380,7 +380,7 @@ def run(tmp, j2):
|
|||||||
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
if not WINDOWS:
|
if not WINDOWS:
|
||||||
msg("\033[31mflock:", repr(ex))
|
msg("\033[31mflock:{!r}\033[0m".format(ex))
|
||||||
|
|
||||||
t = threading.Thread(target=utime, args=(tmp,))
|
t = threading.Thread(target=utime, args=(tmp,))
|
||||||
t.daemon = True
|
t.daemon = True
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ def tc1():
|
|||||||
pdirs = [x.replace("\\", "/") for x in pdirs]
|
pdirs = [x.replace("\\", "/") for x in pdirs]
|
||||||
udirs = [x.split("/", 2)[2] for x in pdirs]
|
udirs = [x.split("/", 2)[2] for x in pdirs]
|
||||||
perms = [x.rstrip("j/")[-1] for x in pdirs]
|
perms = [x.rstrip("j/")[-1] for x in pdirs]
|
||||||
|
perms = ["rw" if x == "a" else x for x in perms]
|
||||||
for pd, ud, p in zip(pdirs, udirs, perms):
|
for pd, ud, p in zip(pdirs, udirs, perms):
|
||||||
if ud[-1] == "j":
|
if ud[-1] == "j":
|
||||||
continue
|
continue
|
||||||
@@ -124,7 +125,7 @@ def tc1():
|
|||||||
|
|
||||||
arg = "{}:{}:{}".format(pd, ud, p, hp)
|
arg = "{}:{}:{}".format(pd, ud, p, hp)
|
||||||
if hp:
|
if hp:
|
||||||
arg += ":chist=" + hp
|
arg += ":c,hist=" + hp
|
||||||
|
|
||||||
args += ["-v", arg]
|
args += ["-v", arg]
|
||||||
|
|
||||||
@@ -147,14 +148,14 @@ def tc1():
|
|||||||
u = "{}{}/a.h264".format(ub, d)
|
u = "{}{}/a.h264".format(ub, d)
|
||||||
r = requests.get(u)
|
r = requests.get(u)
|
||||||
ok = bool(r)
|
ok = bool(r)
|
||||||
if ok != (p in ["a"]):
|
if ok != (p in ["rw"]):
|
||||||
raise Exception("get {} with perm {} at {}".format(ok, p, u))
|
raise Exception("get {} with perm {} at {}".format(ok, p, u))
|
||||||
|
|
||||||
# stat filesystem
|
# stat filesystem
|
||||||
for d, p in zip(pdirs, perms):
|
for d, p in zip(pdirs, perms):
|
||||||
u = "{}/a.h264".format(d)
|
u = "{}/a.h264".format(d)
|
||||||
ok = os.path.exists(u)
|
ok = os.path.exists(u)
|
||||||
if ok != (p in ["a", "w"]):
|
if ok != (p in ["rw", "w"]):
|
||||||
raise Exception("stat {} with perm {} at {}".format(ok, p, u))
|
raise Exception("stat {} with perm {} at {}".format(ok, p, u))
|
||||||
|
|
||||||
# GET thumbnail, vreify contents
|
# GET thumbnail, vreify contents
|
||||||
@@ -162,7 +163,7 @@ def tc1():
|
|||||||
u = "{}{}/a.h264?th=j".format(ub, d)
|
u = "{}{}/a.h264?th=j".format(ub, d)
|
||||||
r = requests.get(u)
|
r = requests.get(u)
|
||||||
ok = bool(r and r.content[:3] == b"\xff\xd8\xff")
|
ok = bool(r and r.content[:3] == b"\xff\xd8\xff")
|
||||||
if ok != (p in ["a"]):
|
if ok != (p in ["rw"]):
|
||||||
raise Exception("thumb {} with perm {} at {}".format(ok, p, u))
|
raise Exception("thumb {} with perm {} at {}".format(ok, p, u))
|
||||||
|
|
||||||
# check tags
|
# check tags
|
||||||
@@ -179,10 +180,10 @@ def tc1():
|
|||||||
r_ok = bool(j)
|
r_ok = bool(j)
|
||||||
w_ok = bool(r_ok and j.get("files"))
|
w_ok = bool(r_ok and j.get("files"))
|
||||||
|
|
||||||
if not r_ok or w_ok != (p in ["a"]):
|
if not r_ok or w_ok != (p in ["rw"]):
|
||||||
raise Exception("ls {} with perm {} at {}".format(ok, p, u))
|
raise Exception("ls {} with perm {} at {}".format(ok, p, u))
|
||||||
|
|
||||||
if (tag and p != "a") or (not tag and p == "a"):
|
if (tag and p != "rw") or (not tag and p == "rw"):
|
||||||
raise Exception("tag {} with perm {} at {}".format(tag, p, u))
|
raise Exception("tag {} with perm {} at {}".format(tag, p, u))
|
||||||
|
|
||||||
if tag is not None and tag != "48x32":
|
if tag is not None and tag != "48x32":
|
||||||
|
|||||||
@@ -65,9 +65,9 @@ def uncomment(fpath):
|
|||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
print("uncommenting", end="")
|
print("uncommenting", end="", flush=True)
|
||||||
for f in sys.argv[1:]:
|
for f in sys.argv[1:]:
|
||||||
print(".", end="")
|
print(".", end="", flush=True)
|
||||||
uncomment(f)
|
uncomment(f)
|
||||||
|
|
||||||
print("k")
|
print("k")
|
||||||
|
|||||||
6
setup.py
6
setup.py
@@ -61,7 +61,7 @@ class clean2(Command):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
nuke = []
|
nuke = []
|
||||||
for (dirpath, dirnames, filenames) in os.walk("."):
|
for (dirpath, _, filenames) in os.walk("."):
|
||||||
for fn in filenames:
|
for fn in filenames:
|
||||||
if (
|
if (
|
||||||
fn.startswith("MANIFEST")
|
fn.startswith("MANIFEST")
|
||||||
@@ -86,7 +86,7 @@ args = {
|
|||||||
"url": "https://github.com/9001/copyparty",
|
"url": "https://github.com/9001/copyparty",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"classifiers": [
|
"classifiers": [
|
||||||
"Development Status :: 4 - Beta",
|
"Development Status :: 5 - Production/Stable",
|
||||||
"License :: OSI Approved :: MIT License",
|
"License :: OSI Approved :: MIT License",
|
||||||
"Programming Language :: Python",
|
"Programming Language :: Python",
|
||||||
"Programming Language :: Python :: 2",
|
"Programming Language :: Python :: 2",
|
||||||
@@ -99,7 +99,9 @@ args = {
|
|||||||
"Programming Language :: Python :: 3.7",
|
"Programming Language :: Python :: 3.7",
|
||||||
"Programming Language :: Python :: 3.8",
|
"Programming Language :: Python :: 3.8",
|
||||||
"Programming Language :: Python :: 3.9",
|
"Programming Language :: Python :: 3.9",
|
||||||
|
"Programming Language :: Python :: 3.10",
|
||||||
"Programming Language :: Python :: Implementation :: CPython",
|
"Programming Language :: Python :: Implementation :: CPython",
|
||||||
|
"Programming Language :: Python :: Implementation :: Jython",
|
||||||
"Programming Language :: Python :: Implementation :: PyPy",
|
"Programming Language :: Python :: Implementation :: PyPy",
|
||||||
"Environment :: Console",
|
"Environment :: Console",
|
||||||
"Environment :: No Input/Output (Daemon)",
|
"Environment :: No Input/Output (Daemon)",
|
||||||
|
|||||||
@@ -23,22 +23,28 @@ def hdr(query):
|
|||||||
|
|
||||||
|
|
||||||
class Cfg(Namespace):
|
class Cfg(Namespace):
|
||||||
def __init__(self, a=[], v=[], c=None):
|
def __init__(self, a=None, v=None, c=None):
|
||||||
super(Cfg, self).__init__(
|
super(Cfg, self).__init__(
|
||||||
a=a,
|
a=a or [],
|
||||||
v=v,
|
v=v or [],
|
||||||
c=c,
|
c=c,
|
||||||
rproxy=0,
|
rproxy=0,
|
||||||
ed=False,
|
ed=False,
|
||||||
nw=False,
|
nw=False,
|
||||||
|
unpost=600,
|
||||||
|
no_mv=False,
|
||||||
|
no_del=False,
|
||||||
no_zip=False,
|
no_zip=False,
|
||||||
|
no_voldump=True,
|
||||||
no_scandir=False,
|
no_scandir=False,
|
||||||
no_sendfile=True,
|
no_sendfile=True,
|
||||||
no_rescan=True,
|
no_rescan=True,
|
||||||
|
re_maxage=0,
|
||||||
ihead=False,
|
ihead=False,
|
||||||
nih=True,
|
nih=True,
|
||||||
mtp=[],
|
mtp=[],
|
||||||
mte="a",
|
mte="a",
|
||||||
|
mth="",
|
||||||
hist=None,
|
hist=None,
|
||||||
no_hash=False,
|
no_hash=False,
|
||||||
css_browser=None,
|
css_browser=None,
|
||||||
@@ -90,7 +96,7 @@ class TestHttpCli(unittest.TestCase):
|
|||||||
if not vol.startswith(top):
|
if not vol.startswith(top):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
mode = vol[-2]
|
mode = vol[-2].replace("a", "rwmd")
|
||||||
usr = vol[-1]
|
usr = vol[-1]
|
||||||
if usr == "a":
|
if usr == "a":
|
||||||
usr = ""
|
usr = ""
|
||||||
@@ -99,7 +105,7 @@ class TestHttpCli(unittest.TestCase):
|
|||||||
vol += "/"
|
vol += "/"
|
||||||
|
|
||||||
top, sub = vol.split("/", 1)
|
top, sub = vol.split("/", 1)
|
||||||
vcfg.append("{0}/{1}:{1}:{2}{3}".format(top, sub, mode, usr))
|
vcfg.append("{0}/{1}:{1}:{2},{3}".format(top, sub, mode, usr))
|
||||||
|
|
||||||
pprint.pprint(vcfg)
|
pprint.pprint(vcfg)
|
||||||
|
|
||||||
|
|||||||
@@ -16,18 +16,21 @@ from copyparty import util
|
|||||||
|
|
||||||
|
|
||||||
class Cfg(Namespace):
|
class Cfg(Namespace):
|
||||||
def __init__(self, a=[], v=[], c=None):
|
def __init__(self, a=None, v=None, c=None):
|
||||||
ex = {k: False for k in "nw e2d e2ds e2dsa e2t e2ts e2tsr".split()}
|
ex = {k: False for k in "nw e2d e2ds e2dsa e2t e2ts e2tsr".split()}
|
||||||
ex2 = {
|
ex2 = {
|
||||||
"mtp": [],
|
"mtp": [],
|
||||||
"mte": "a",
|
"mte": "a",
|
||||||
|
"mth": "",
|
||||||
"hist": None,
|
"hist": None,
|
||||||
"no_hash": False,
|
"no_hash": False,
|
||||||
"css_browser": None,
|
"css_browser": None,
|
||||||
|
"no_voldump": True,
|
||||||
|
"re_maxage": 0,
|
||||||
"rproxy": 0,
|
"rproxy": 0,
|
||||||
}
|
}
|
||||||
ex.update(ex2)
|
ex.update(ex2)
|
||||||
super(Cfg, self).__init__(a=a, v=v, c=c, **ex)
|
super(Cfg, self).__init__(a=a or [], v=v or [], c=c, **ex)
|
||||||
|
|
||||||
|
|
||||||
class TestVFS(unittest.TestCase):
|
class TestVFS(unittest.TestCase):
|
||||||
@@ -57,8 +60,8 @@ class TestVFS(unittest.TestCase):
|
|||||||
# type: (VFS, str, str) -> tuple[str, str, str]
|
# type: (VFS, str, str) -> tuple[str, str, str]
|
||||||
"""helper for resolving and listing a folder"""
|
"""helper for resolving and listing a folder"""
|
||||||
vn, rem = vfs.get(vpath, uname, True, False)
|
vn, rem = vfs.get(vpath, uname, True, False)
|
||||||
r1 = vn.ls(rem, uname, False)
|
r1 = vn.ls(rem, uname, False, [[True]])
|
||||||
r2 = vn.ls(rem, uname, False)
|
r2 = vn.ls(rem, uname, False, [[True]])
|
||||||
self.assertEqual(r1, r2)
|
self.assertEqual(r1, r2)
|
||||||
|
|
||||||
fsdir, real, virt = r1
|
fsdir, real, virt = r1
|
||||||
@@ -68,6 +71,11 @@ class TestVFS(unittest.TestCase):
|
|||||||
def log(self, src, msg, c=0):
|
def log(self, src, msg, c=0):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def assertAxs(self, dct, lst):
|
||||||
|
t1 = list(sorted(dct.keys()))
|
||||||
|
t2 = list(sorted(lst))
|
||||||
|
self.assertEqual(t1, t2)
|
||||||
|
|
||||||
def test(self):
|
def test(self):
|
||||||
td = os.path.join(self.td, "vfs")
|
td = os.path.join(self.td, "vfs")
|
||||||
os.mkdir(td)
|
os.mkdir(td)
|
||||||
@@ -88,53 +96,53 @@ class TestVFS(unittest.TestCase):
|
|||||||
self.assertEqual(vfs.nodes, {})
|
self.assertEqual(vfs.nodes, {})
|
||||||
self.assertEqual(vfs.vpath, "")
|
self.assertEqual(vfs.vpath, "")
|
||||||
self.assertEqual(vfs.realpath, td)
|
self.assertEqual(vfs.realpath, td)
|
||||||
self.assertEqual(vfs.uread, ["*"])
|
self.assertAxs(vfs.axs.uread, ["*"])
|
||||||
self.assertEqual(vfs.uwrite, ["*"])
|
self.assertAxs(vfs.axs.uwrite, ["*"])
|
||||||
|
|
||||||
# single read-only rootfs (relative path)
|
# single read-only rootfs (relative path)
|
||||||
vfs = AuthSrv(Cfg(v=["a/ab/::r"]), self.log).vfs
|
vfs = AuthSrv(Cfg(v=["a/ab/::r"]), self.log).vfs
|
||||||
self.assertEqual(vfs.nodes, {})
|
self.assertEqual(vfs.nodes, {})
|
||||||
self.assertEqual(vfs.vpath, "")
|
self.assertEqual(vfs.vpath, "")
|
||||||
self.assertEqual(vfs.realpath, os.path.join(td, "a", "ab"))
|
self.assertEqual(vfs.realpath, os.path.join(td, "a", "ab"))
|
||||||
self.assertEqual(vfs.uread, ["*"])
|
self.assertAxs(vfs.axs.uread, ["*"])
|
||||||
self.assertEqual(vfs.uwrite, [])
|
self.assertAxs(vfs.axs.uwrite, [])
|
||||||
|
|
||||||
# single read-only rootfs (absolute path)
|
# single read-only rootfs (absolute path)
|
||||||
vfs = AuthSrv(Cfg(v=[td + "//a/ac/../aa//::r"]), self.log).vfs
|
vfs = AuthSrv(Cfg(v=[td + "//a/ac/../aa//::r"]), self.log).vfs
|
||||||
self.assertEqual(vfs.nodes, {})
|
self.assertEqual(vfs.nodes, {})
|
||||||
self.assertEqual(vfs.vpath, "")
|
self.assertEqual(vfs.vpath, "")
|
||||||
self.assertEqual(vfs.realpath, os.path.join(td, "a", "aa"))
|
self.assertEqual(vfs.realpath, os.path.join(td, "a", "aa"))
|
||||||
self.assertEqual(vfs.uread, ["*"])
|
self.assertAxs(vfs.axs.uread, ["*"])
|
||||||
self.assertEqual(vfs.uwrite, [])
|
self.assertAxs(vfs.axs.uwrite, [])
|
||||||
|
|
||||||
# read-only rootfs with write-only subdirectory (read-write for k)
|
# read-only rootfs with write-only subdirectory (read-write for k)
|
||||||
vfs = AuthSrv(
|
vfs = AuthSrv(
|
||||||
Cfg(a=["k:k"], v=[".::r:ak", "a/ac/acb:a/ac/acb:w:ak"]),
|
Cfg(a=["k:k"], v=[".::r:rw,k", "a/ac/acb:a/ac/acb:w:rw,k"]),
|
||||||
self.log,
|
self.log,
|
||||||
).vfs
|
).vfs
|
||||||
self.assertEqual(len(vfs.nodes), 1)
|
self.assertEqual(len(vfs.nodes), 1)
|
||||||
self.assertEqual(vfs.vpath, "")
|
self.assertEqual(vfs.vpath, "")
|
||||||
self.assertEqual(vfs.realpath, td)
|
self.assertEqual(vfs.realpath, td)
|
||||||
self.assertEqual(vfs.uread, ["*", "k"])
|
self.assertAxs(vfs.axs.uread, ["*", "k"])
|
||||||
self.assertEqual(vfs.uwrite, ["k"])
|
self.assertAxs(vfs.axs.uwrite, ["k"])
|
||||||
n = vfs.nodes["a"]
|
n = vfs.nodes["a"]
|
||||||
self.assertEqual(len(vfs.nodes), 1)
|
self.assertEqual(len(vfs.nodes), 1)
|
||||||
self.assertEqual(n.vpath, "a")
|
self.assertEqual(n.vpath, "a")
|
||||||
self.assertEqual(n.realpath, os.path.join(td, "a"))
|
self.assertEqual(n.realpath, os.path.join(td, "a"))
|
||||||
self.assertEqual(n.uread, ["*", "k"])
|
self.assertAxs(n.axs.uread, ["*", "k"])
|
||||||
self.assertEqual(n.uwrite, ["k"])
|
self.assertAxs(n.axs.uwrite, ["k"])
|
||||||
n = n.nodes["ac"]
|
n = n.nodes["ac"]
|
||||||
self.assertEqual(len(vfs.nodes), 1)
|
self.assertEqual(len(vfs.nodes), 1)
|
||||||
self.assertEqual(n.vpath, "a/ac")
|
self.assertEqual(n.vpath, "a/ac")
|
||||||
self.assertEqual(n.realpath, os.path.join(td, "a", "ac"))
|
self.assertEqual(n.realpath, os.path.join(td, "a", "ac"))
|
||||||
self.assertEqual(n.uread, ["*", "k"])
|
self.assertAxs(n.axs.uread, ["*", "k"])
|
||||||
self.assertEqual(n.uwrite, ["k"])
|
self.assertAxs(n.axs.uwrite, ["k"])
|
||||||
n = n.nodes["acb"]
|
n = n.nodes["acb"]
|
||||||
self.assertEqual(n.nodes, {})
|
self.assertEqual(n.nodes, {})
|
||||||
self.assertEqual(n.vpath, "a/ac/acb")
|
self.assertEqual(n.vpath, "a/ac/acb")
|
||||||
self.assertEqual(n.realpath, os.path.join(td, "a", "ac", "acb"))
|
self.assertEqual(n.realpath, os.path.join(td, "a", "ac", "acb"))
|
||||||
self.assertEqual(n.uread, ["k"])
|
self.assertAxs(n.axs.uread, ["k"])
|
||||||
self.assertEqual(n.uwrite, ["*", "k"])
|
self.assertAxs(n.axs.uwrite, ["*", "k"])
|
||||||
|
|
||||||
# something funky about the windows path normalization,
|
# something funky about the windows path normalization,
|
||||||
# doesn't really matter but makes the test messy, TODO?
|
# doesn't really matter but makes the test messy, TODO?
|
||||||
@@ -173,24 +181,24 @@ class TestVFS(unittest.TestCase):
|
|||||||
|
|
||||||
# admin-only rootfs with all-read-only subfolder
|
# admin-only rootfs with all-read-only subfolder
|
||||||
vfs = AuthSrv(
|
vfs = AuthSrv(
|
||||||
Cfg(a=["k:k"], v=[".::ak", "a:a:r"]),
|
Cfg(a=["k:k"], v=[".::rw,k", "a:a:r"]),
|
||||||
self.log,
|
self.log,
|
||||||
).vfs
|
).vfs
|
||||||
self.assertEqual(len(vfs.nodes), 1)
|
self.assertEqual(len(vfs.nodes), 1)
|
||||||
self.assertEqual(vfs.vpath, "")
|
self.assertEqual(vfs.vpath, "")
|
||||||
self.assertEqual(vfs.realpath, td)
|
self.assertEqual(vfs.realpath, td)
|
||||||
self.assertEqual(vfs.uread, ["k"])
|
self.assertAxs(vfs.axs.uread, ["k"])
|
||||||
self.assertEqual(vfs.uwrite, ["k"])
|
self.assertAxs(vfs.axs.uwrite, ["k"])
|
||||||
n = vfs.nodes["a"]
|
n = vfs.nodes["a"]
|
||||||
self.assertEqual(len(vfs.nodes), 1)
|
self.assertEqual(len(vfs.nodes), 1)
|
||||||
self.assertEqual(n.vpath, "a")
|
self.assertEqual(n.vpath, "a")
|
||||||
self.assertEqual(n.realpath, os.path.join(td, "a"))
|
self.assertEqual(n.realpath, os.path.join(td, "a"))
|
||||||
self.assertEqual(n.uread, ["*"])
|
self.assertAxs(n.axs.uread, ["*"])
|
||||||
self.assertEqual(n.uwrite, [])
|
self.assertAxs(n.axs.uwrite, [])
|
||||||
self.assertEqual(vfs.can_access("/", "*"), [False, False])
|
self.assertEqual(vfs.can_access("/", "*"), [False, False, False, False])
|
||||||
self.assertEqual(vfs.can_access("/", "k"), [True, True])
|
self.assertEqual(vfs.can_access("/", "k"), [True, True, False, False])
|
||||||
self.assertEqual(vfs.can_access("/a", "*"), [True, False])
|
self.assertEqual(vfs.can_access("/a", "*"), [True, False, False, False])
|
||||||
self.assertEqual(vfs.can_access("/a", "k"), [True, False])
|
self.assertEqual(vfs.can_access("/a", "k"), [True, False, False, False])
|
||||||
|
|
||||||
# breadth-first construction
|
# breadth-first construction
|
||||||
vfs = AuthSrv(
|
vfs = AuthSrv(
|
||||||
@@ -247,26 +255,26 @@ class TestVFS(unittest.TestCase):
|
|||||||
./src
|
./src
|
||||||
/dst
|
/dst
|
||||||
r a
|
r a
|
||||||
a asd
|
rw asd
|
||||||
"""
|
"""
|
||||||
).encode("utf-8")
|
).encode("utf-8")
|
||||||
)
|
)
|
||||||
|
|
||||||
au = AuthSrv(Cfg(c=[cfg_path]), self.log)
|
au = AuthSrv(Cfg(c=[cfg_path]), self.log)
|
||||||
self.assertEqual(au.user["a"], "123")
|
self.assertEqual(au.acct["a"], "123")
|
||||||
self.assertEqual(au.user["asd"], "fgh:jkl")
|
self.assertEqual(au.acct["asd"], "fgh:jkl")
|
||||||
n = au.vfs
|
n = au.vfs
|
||||||
# root was not defined, so PWD with no access to anyone
|
# root was not defined, so PWD with no access to anyone
|
||||||
self.assertEqual(n.vpath, "")
|
self.assertEqual(n.vpath, "")
|
||||||
self.assertEqual(n.realpath, None)
|
self.assertEqual(n.realpath, None)
|
||||||
self.assertEqual(n.uread, [])
|
self.assertAxs(n.axs.uread, [])
|
||||||
self.assertEqual(n.uwrite, [])
|
self.assertAxs(n.axs.uwrite, [])
|
||||||
self.assertEqual(len(n.nodes), 1)
|
self.assertEqual(len(n.nodes), 1)
|
||||||
n = n.nodes["dst"]
|
n = n.nodes["dst"]
|
||||||
self.assertEqual(n.vpath, "dst")
|
self.assertEqual(n.vpath, "dst")
|
||||||
self.assertEqual(n.realpath, os.path.join(td, "src"))
|
self.assertEqual(n.realpath, os.path.join(td, "src"))
|
||||||
self.assertEqual(n.uread, ["a", "asd"])
|
self.assertAxs(n.axs.uread, ["a", "asd"])
|
||||||
self.assertEqual(n.uwrite, ["asd"])
|
self.assertAxs(n.axs.uwrite, ["asd"])
|
||||||
self.assertEqual(len(n.nodes), 0)
|
self.assertEqual(len(n.nodes), 0)
|
||||||
|
|
||||||
os.unlink(cfg_path)
|
os.unlink(cfg_path)
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ if MACOS:
|
|||||||
from copyparty.util import Unrecv
|
from copyparty.util import Unrecv
|
||||||
|
|
||||||
|
|
||||||
def runcmd(*argv):
|
def runcmd(argv):
|
||||||
p = sp.Popen(argv, stdout=sp.PIPE, stderr=sp.PIPE)
|
p = sp.Popen(argv, stdout=sp.PIPE, stderr=sp.PIPE)
|
||||||
stdout, stderr = p.communicate()
|
stdout, stderr = p.communicate()
|
||||||
stdout = stdout.decode("utf-8")
|
stdout = stdout.decode("utf-8")
|
||||||
@@ -39,8 +39,8 @@ def runcmd(*argv):
|
|||||||
return [p.returncode, stdout, stderr]
|
return [p.returncode, stdout, stderr]
|
||||||
|
|
||||||
|
|
||||||
def chkcmd(*argv):
|
def chkcmd(argv):
|
||||||
ok, sout, serr = runcmd(*argv)
|
ok, sout, serr = runcmd(argv)
|
||||||
if ok != 0:
|
if ok != 0:
|
||||||
raise Exception(serr)
|
raise Exception(serr)
|
||||||
|
|
||||||
@@ -60,12 +60,20 @@ def get_ramdisk():
|
|||||||
|
|
||||||
if os.path.exists("/Volumes"):
|
if os.path.exists("/Volumes"):
|
||||||
# hdiutil eject /Volumes/cptd/
|
# hdiutil eject /Volumes/cptd/
|
||||||
devname, _ = chkcmd("hdiutil", "attach", "-nomount", "ram://131072")
|
devname, _ = chkcmd("hdiutil attach -nomount ram://131072".split())
|
||||||
devname = devname.strip()
|
devname = devname.strip()
|
||||||
print("devname: [{}]".format(devname))
|
print("devname: [{}]".format(devname))
|
||||||
for _ in range(10):
|
for _ in range(10):
|
||||||
try:
|
try:
|
||||||
_, _ = chkcmd("diskutil", "eraseVolume", "HFS+", "cptd", devname)
|
_, _ = chkcmd(["diskutil", "eraseVolume", "HFS+", "cptd", devname])
|
||||||
|
with open("/Volumes/cptd/.metadata_never_index", "w") as f:
|
||||||
|
f.write("orz")
|
||||||
|
|
||||||
|
try:
|
||||||
|
shutil.rmtree("/Volumes/cptd/.fseventsd")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
return subdir("/Volumes/cptd")
|
return subdir("/Volumes/cptd")
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print(repr(ex))
|
print(repr(ex))
|
||||||
@@ -119,14 +127,13 @@ class VHttpConn(object):
|
|||||||
self.addr = ("127.0.0.1", "42069")
|
self.addr = ("127.0.0.1", "42069")
|
||||||
self.args = args
|
self.args = args
|
||||||
self.asrv = asrv
|
self.asrv = asrv
|
||||||
self.is_mp = False
|
self.nid = None
|
||||||
self.log_func = log
|
self.log_func = log
|
||||||
self.log_src = "a"
|
self.log_src = "a"
|
||||||
self.lf_url = None
|
self.lf_url = None
|
||||||
self.hsrv = VHttpSrv()
|
self.hsrv = VHttpSrv()
|
||||||
self.nreq = 0
|
self.nreq = 0
|
||||||
self.nbyte = 0
|
self.nbyte = 0
|
||||||
self.workload = 0
|
|
||||||
self.ico = None
|
self.ico = None
|
||||||
self.thumbcli = None
|
self.thumbcli = None
|
||||||
self.t0 = time.time()
|
self.t0 = time.time()
|
||||||
Reference in New Issue
Block a user