mirror of
https://github.com/9001/copyparty.git
synced 2025-10-31 20:13:34 +00:00
Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
755a2ee023 | ||
|
|
69d3359e47 | ||
|
|
a90c49b8fb | ||
|
|
b1222edb27 | ||
|
|
b967a92f69 | ||
|
|
90a5cb5e59 | ||
|
|
7aba9cb76b | ||
|
|
f550a8171d | ||
|
|
82e568d4c9 | ||
|
|
7b2a4a3d59 | ||
|
|
0265455cd1 | ||
|
|
afafc886a4 | ||
|
|
8a959f6ac4 | ||
|
|
1c3aa0d2c5 | ||
|
|
79b7d3316a | ||
|
|
fa7768583a | ||
|
|
faf49f6c15 | ||
|
|
765af31b83 | ||
|
|
b6a3c52d67 | ||
|
|
b025c2f660 | ||
|
|
e559a7c878 | ||
|
|
5c8855aafd | ||
|
|
b5fc537b89 | ||
|
|
14899d3a7c | ||
|
|
0ea7881652 | ||
|
|
ec29b59d1e | ||
|
|
9405597c15 | ||
|
|
82441978c6 | ||
|
|
e0e6291bdb | ||
|
|
b2b083fd0a | ||
|
|
f8a51b68e7 | ||
|
|
e0a19108e5 | ||
|
|
770ea68ca8 | ||
|
|
ce36c52baf | ||
|
|
a7da1dd233 | ||
|
|
678ef296b4 | ||
|
|
9e5627d805 | ||
|
|
5958ee4439 | ||
|
|
7127e57f0e | ||
|
|
ee9c6dc8aa | ||
|
|
92779b3f48 | ||
|
|
2f1baf17d4 | ||
|
|
583da3d4a9 | ||
|
|
bf9ff78bcc | ||
|
|
2cb07792cc | ||
|
|
47bc8bb466 | ||
|
|
94ad1f5732 | ||
|
|
09557fbe83 | ||
|
|
1c0f44fa4e | ||
|
|
fc4d59d2d7 | ||
|
|
12345fbacc | ||
|
|
2e33c8d222 |
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
@@ -9,8 +9,6 @@
|
|||||||
"console": "integratedTerminal",
|
"console": "integratedTerminal",
|
||||||
"cwd": "${workspaceFolder}",
|
"cwd": "${workspaceFolder}",
|
||||||
"args": [
|
"args": [
|
||||||
"-j",
|
|
||||||
"0",
|
|
||||||
//"-nw",
|
//"-nw",
|
||||||
"-a",
|
"-a",
|
||||||
"ed:wark",
|
"ed:wark",
|
||||||
|
|||||||
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@@ -37,7 +37,7 @@
|
|||||||
"python.linting.banditEnabled": true,
|
"python.linting.banditEnabled": true,
|
||||||
"python.linting.flake8Args": [
|
"python.linting.flake8Args": [
|
||||||
"--max-line-length=120",
|
"--max-line-length=120",
|
||||||
"--ignore=E722,F405,E203,W503,W293",
|
"--ignore=E722,F405,E203,W503,W293,E402",
|
||||||
],
|
],
|
||||||
"python.linting.banditArgs": [
|
"python.linting.banditArgs": [
|
||||||
"--ignore=B104"
|
"--ignore=B104"
|
||||||
@@ -55,6 +55,6 @@
|
|||||||
//
|
//
|
||||||
// things you may wanna edit:
|
// things you may wanna edit:
|
||||||
//
|
//
|
||||||
"python.pythonPath": ".venv/bin/python",
|
"python.pythonPath": "/usr/bin/python3",
|
||||||
//"python.linting.enabled": true,
|
//"python.linting.enabled": true,
|
||||||
}
|
}
|
||||||
19
README.md
19
README.md
@@ -19,6 +19,8 @@ turn your phone or raspi into a portable file server with resumable uploads/down
|
|||||||
* Android-Chrome: set max "parallel uploads" for 200% upload speed (android bug)
|
* Android-Chrome: set max "parallel uploads" for 200% upload speed (android bug)
|
||||||
* Android-Firefox: takes a while to select files (in order to avoid the above android-chrome issue)
|
* Android-Firefox: takes a while to select files (in order to avoid the above android-chrome issue)
|
||||||
* Desktop-Firefox: may use gigabytes of RAM if your connection is great and your files are massive
|
* Desktop-Firefox: may use gigabytes of RAM if your connection is great and your files are massive
|
||||||
|
* paper-printing is affected by dark/light-mode! use lightmode for color, darkmode for grayscale
|
||||||
|
* because no browsers currently implement the media-query to do this properly orz
|
||||||
|
|
||||||
|
|
||||||
## status
|
## status
|
||||||
@@ -36,10 +38,22 @@ turn your phone or raspi into a portable file server with resumable uploads/down
|
|||||||
* [x] accounts
|
* [x] accounts
|
||||||
* [x] markdown viewer
|
* [x] markdown viewer
|
||||||
* [x] markdown editor
|
* [x] markdown editor
|
||||||
|
* [x] FUSE client (read-only)
|
||||||
|
|
||||||
summary: it works! you can use it! (but technically not even close to beta)
|
summary: it works! you can use it! (but technically not even close to beta)
|
||||||
|
|
||||||
|
|
||||||
|
# client examples
|
||||||
|
|
||||||
|
* javascript: dump some state into a file (two separate examples)
|
||||||
|
* `await fetch('https://127.0.0.1:3923/', {method:"PUT", body: JSON.stringify(foo)});`
|
||||||
|
* `var xhr = new XMLHttpRequest(); xhr.open('POST', 'https://127.0.0.1:3923/msgs?raw'); xhr.send('foo');`
|
||||||
|
|
||||||
|
* FUSE: mount a copyparty server as a local filesystem
|
||||||
|
* cross-platform python client available in [./bin/](bin/)
|
||||||
|
* [rclone](https://rclone.org/) as client can give ~5x performance, see [./docs/rclone.md](docs/rclone.md)
|
||||||
|
|
||||||
|
|
||||||
# dependencies
|
# dependencies
|
||||||
|
|
||||||
* `jinja2`
|
* `jinja2`
|
||||||
@@ -55,10 +69,13 @@ currently there are two self-contained binaries:
|
|||||||
* `copyparty-sfx.sh` for unix (linux and osx) -- smaller, more robust
|
* `copyparty-sfx.sh` for unix (linux and osx) -- smaller, more robust
|
||||||
* `copyparty-sfx.py` for windows (unix too) -- crossplatform, beta
|
* `copyparty-sfx.py` for windows (unix too) -- crossplatform, beta
|
||||||
|
|
||||||
launch either of them and it'll unpack and run copyparty, assuming you have python installed of course
|
launch either of them (**use sfx.py on systemd**) and it'll unpack and run copyparty, assuming you have python installed of course
|
||||||
|
|
||||||
pls note that `copyparty-sfx.sh` will fail if you rename `copyparty-sfx.py` to `copyparty.py` and keep it in the same folder because `sys.path` is funky
|
pls note that `copyparty-sfx.sh` will fail if you rename `copyparty-sfx.py` to `copyparty.py` and keep it in the same folder because `sys.path` is funky
|
||||||
|
|
||||||
|
|
||||||
|
## sfx repack
|
||||||
|
|
||||||
if you don't need all the features you can repack the sfx and save a bunch of space; all you need is an sfx and a copy of this repo (nothing else to download or build, except for either msys2 or WSL if you're on windows)
|
if you don't need all the features you can repack the sfx and save a bunch of space; all you need is an sfx and a copy of this repo (nothing else to download or build, except for either msys2 or WSL if you're on windows)
|
||||||
* `724K` original size as of v0.4.0
|
* `724K` original size as of v0.4.0
|
||||||
* `256K` after `./scripts/make-sfx.sh re no-ogv`
|
* `256K` after `./scripts/make-sfx.sh re no-ogv`
|
||||||
|
|||||||
36
bin/README.md
Normal file
36
bin/README.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# copyparty-fuse.py
|
||||||
|
* mount a copyparty server as a local filesystem (read-only)
|
||||||
|
* **supports Windows!** -- expect `194 MiB/s` sequential read
|
||||||
|
* **supports Linux** -- expect `117 MiB/s` sequential read
|
||||||
|
* **supports macos** -- expect `85 MiB/s` sequential read
|
||||||
|
|
||||||
|
filecache is default-on for windows and macos;
|
||||||
|
* macos readsize is 64kB, so speed ~32 MiB/s without the cache
|
||||||
|
* windows readsize varies by software; explorer=1M, pv=32k
|
||||||
|
|
||||||
|
note that copyparty should run with `-ed` to enable dotfiles (hidden otherwise)
|
||||||
|
|
||||||
|
also consider using [../docs/rclone.md](../docs/rclone.md) instead for 5x performance
|
||||||
|
|
||||||
|
|
||||||
|
## to run this on windows:
|
||||||
|
* install [winfsp](https://github.com/billziss-gh/winfsp/releases/latest) and [python 3](https://www.python.org/downloads/)
|
||||||
|
* [x] add python 3.x to PATH (it asks during install)
|
||||||
|
* `python -m pip install --user fusepy`
|
||||||
|
* `python ./copyparty-fuse.py n: http://192.168.1.69:3923/`
|
||||||
|
|
||||||
|
10% faster in [msys2](https://www.msys2.org/), 700% faster if debug prints are enabled:
|
||||||
|
* `pacman -S mingw64/mingw-w64-x86_64-python{,-pip}`
|
||||||
|
* `/mingw64/bin/python3 -m pip install --user fusepy`
|
||||||
|
* `/mingw64/bin/python3 ./copyparty-fuse.py [...]`
|
||||||
|
|
||||||
|
you could replace winfsp with [dokan](https://github.com/dokan-dev/dokany/releases/latest), let me know if you [figure out how](https://github.com/dokan-dev/dokany/wiki/FUSE)
|
||||||
|
(winfsp's sshfs leaks, doesn't look like winfsp itself does, should be fine)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# copyparty-fuse🅱️.py
|
||||||
|
* mount a copyparty server as a local filesystem (read-only)
|
||||||
|
* does the same thing except more correct, `samba` approves
|
||||||
|
* **supports Linux** -- expect `18 MiB/s` (wait what)
|
||||||
|
* **supports Macos** -- probably
|
||||||
@@ -7,47 +7,83 @@ __copyright__ = 2019
|
|||||||
__license__ = "MIT"
|
__license__ = "MIT"
|
||||||
__url__ = "https://github.com/9001/copyparty/"
|
__url__ = "https://github.com/9001/copyparty/"
|
||||||
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
import stat
|
|
||||||
import errno
|
|
||||||
import struct
|
|
||||||
import threading
|
|
||||||
import http.client # py2: httplib
|
|
||||||
import urllib.parse
|
|
||||||
from datetime import datetime
|
|
||||||
from urllib.parse import quote_from_bytes as quote
|
|
||||||
|
|
||||||
try:
|
|
||||||
from fuse import FUSE, FuseOSError, Operations
|
|
||||||
except:
|
|
||||||
print("\n could not import fuse;\n pip install fusepy\n")
|
|
||||||
raise
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
mount a copyparty server (local or remote) as a filesystem
|
mount a copyparty server (local or remote) as a filesystem
|
||||||
|
|
||||||
usage:
|
usage:
|
||||||
python copyparty-fuse.py ./music http://192.168.1.69:1234/
|
python copyparty-fuse.py ./music http://192.168.1.69:3923/
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
sudo apk add fuse-dev
|
python3 -m pip install --user fusepy
|
||||||
python3 -m venv ~/pe/ve.fusepy
|
+ on Linux: sudo apk add fuse
|
||||||
. ~/pe/ve.fusepy/bin/activate
|
+ on Macos: https://osxfuse.github.io/
|
||||||
pip install fusepy
|
+ on Windows: https://github.com/billziss-gh/winfsp/releases/latest
|
||||||
|
|
||||||
|
get server cert:
|
||||||
MB/s
|
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
|
||||||
28 cache NOthread
|
|
||||||
24 cache thread
|
|
||||||
29 cache NOthread NOmutex
|
|
||||||
67 NOcache NOthread NOmutex ( ´・ω・) nyoro~n
|
|
||||||
10 NOcache thread NOmutex
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
import re
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import stat
|
||||||
|
import errno
|
||||||
|
import struct
|
||||||
|
import codecs
|
||||||
|
import builtins
|
||||||
|
import platform
|
||||||
|
import argparse
|
||||||
|
import threading
|
||||||
|
import traceback
|
||||||
|
import http.client # py2: httplib
|
||||||
|
import urllib.parse
|
||||||
|
from datetime import datetime
|
||||||
|
from urllib.parse import quote_from_bytes as quote
|
||||||
|
from urllib.parse import unquote_to_bytes as unquote
|
||||||
|
|
||||||
|
WINDOWS = sys.platform == "win32"
|
||||||
|
MACOS = platform.system() == "Darwin"
|
||||||
|
info = log = dbg = None
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
from fuse import FUSE, FuseOSError, Operations
|
||||||
|
except:
|
||||||
|
if WINDOWS:
|
||||||
|
libfuse = "install https://github.com/billziss-gh/winfsp/releases/latest"
|
||||||
|
elif MACOS:
|
||||||
|
libfuse = "install https://osxfuse.github.io/"
|
||||||
|
else:
|
||||||
|
libfuse = "apt install libfuse\n modprobe fuse"
|
||||||
|
|
||||||
|
print(
|
||||||
|
"\n could not import fuse; these may help:"
|
||||||
|
+ "\n python3 -m pip install --user fusepy\n "
|
||||||
|
+ libfuse
|
||||||
|
+ "\n"
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def print(*args, **kwargs):
|
||||||
|
try:
|
||||||
|
builtins.print(*list(args), **kwargs)
|
||||||
|
except:
|
||||||
|
builtins.print(termsafe(" ".join(str(x) for x in args)), **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def termsafe(txt):
|
||||||
|
try:
|
||||||
|
return txt.encode(sys.stdout.encoding, "backslashreplace").decode(
|
||||||
|
sys.stdout.encoding
|
||||||
|
)
|
||||||
|
except:
|
||||||
|
return txt.encode(sys.stdout.encoding, "replace").decode(sys.stdout.encoding)
|
||||||
|
|
||||||
|
|
||||||
def threadless_log(msg):
|
def threadless_log(msg):
|
||||||
print(msg + "\n", end="")
|
print(msg + "\n", end="")
|
||||||
|
|
||||||
@@ -60,27 +96,127 @@ def boring_log(msg):
|
|||||||
def rice_tid():
|
def rice_tid():
|
||||||
tid = threading.current_thread().ident
|
tid = threading.current_thread().ident
|
||||||
c = struct.unpack(b"B" * 5, struct.pack(b">Q", tid)[-5:])
|
c = struct.unpack(b"B" * 5, struct.pack(b">Q", tid)[-5:])
|
||||||
return "".join("\033[1;37;48;5;{}m{:02x}".format(x, x) for x in c)
|
return "".join("\033[1;37;48;5;{}m{:02x}".format(x, x) for x in c) + "\033[0m"
|
||||||
|
|
||||||
|
|
||||||
def fancy_log(msg):
|
def fancy_log(msg):
|
||||||
print("{}\033[0m {}\n".format(rice_tid(), msg), end="")
|
print("{} {}\n".format(rice_tid(), msg), end="")
|
||||||
|
|
||||||
|
|
||||||
def null_log(msg):
|
def null_log(msg):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
log = boring_log
|
def hexler(binary):
|
||||||
log = fancy_log
|
return binary.replace("\r", "\\r").replace("\n", "\\n")
|
||||||
log = threadless_log
|
return " ".join(["{}\033[36m{:02x}\033[0m".format(b, ord(b)) for b in binary])
|
||||||
dbg = null_log
|
return " ".join(map(lambda b: format(ord(b), "02x"), binary))
|
||||||
|
|
||||||
|
|
||||||
|
def register_wtf8():
|
||||||
|
def wtf8_enc(text):
|
||||||
|
return str(text).encode("utf-8", "surrogateescape"), len(text)
|
||||||
|
|
||||||
|
def wtf8_dec(binary):
|
||||||
|
return bytes(binary).decode("utf-8", "surrogateescape"), len(binary)
|
||||||
|
|
||||||
|
def wtf8_search(encoding_name):
|
||||||
|
return codecs.CodecInfo(wtf8_enc, wtf8_dec, name="wtf-8")
|
||||||
|
|
||||||
|
codecs.register(wtf8_search)
|
||||||
|
|
||||||
|
|
||||||
|
bad_good = {}
|
||||||
|
good_bad = {}
|
||||||
|
|
||||||
|
|
||||||
|
def enwin(txt):
|
||||||
|
return "".join([bad_good.get(x, x) for x in txt])
|
||||||
|
|
||||||
|
for bad, good in bad_good.items():
|
||||||
|
txt = txt.replace(bad, good)
|
||||||
|
|
||||||
|
return txt
|
||||||
|
|
||||||
|
|
||||||
|
def dewin(txt):
|
||||||
|
return "".join([good_bad.get(x, x) for x in txt])
|
||||||
|
|
||||||
|
for bad, good in bad_good.items():
|
||||||
|
txt = txt.replace(good, bad)
|
||||||
|
|
||||||
|
return txt
|
||||||
|
|
||||||
|
|
||||||
|
class RecentLog(object):
|
||||||
|
def __init__(self):
|
||||||
|
self.mtx = threading.Lock()
|
||||||
|
self.f = None # open("copyparty-fuse.log", "wb")
|
||||||
|
self.q = []
|
||||||
|
|
||||||
|
thr = threading.Thread(target=self.printer)
|
||||||
|
thr.daemon = True
|
||||||
|
thr.start()
|
||||||
|
|
||||||
|
def put(self, msg):
|
||||||
|
msg = "{} {}\n".format(rice_tid(), msg)
|
||||||
|
if self.f:
|
||||||
|
fmsg = " ".join([datetime.utcnow().strftime("%H%M%S.%f"), str(msg)])
|
||||||
|
self.f.write(fmsg.encode("utf-8"))
|
||||||
|
|
||||||
|
with self.mtx:
|
||||||
|
self.q.append(msg)
|
||||||
|
if len(self.q) > 200:
|
||||||
|
self.q = self.q[-50:]
|
||||||
|
|
||||||
|
def printer(self):
|
||||||
|
while True:
|
||||||
|
time.sleep(0.05)
|
||||||
|
with self.mtx:
|
||||||
|
q = self.q
|
||||||
|
if not q:
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.q = []
|
||||||
|
|
||||||
|
print("".join(q), end="")
|
||||||
|
|
||||||
|
|
||||||
|
# [windows/cmd/cpy3] python dev\copyparty\bin\copyparty-fuse.py q: http://192.168.1.159:1234/
|
||||||
|
# [windows/cmd/msys2] C:\msys64\mingw64\bin\python3 dev\copyparty\bin\copyparty-fuse.py q: http://192.168.1.159:1234/
|
||||||
|
# [windows/mty/msys2] /mingw64/bin/python3 /c/Users/ed/dev/copyparty/bin/copyparty-fuse.py q: http://192.168.1.159:1234/
|
||||||
|
#
|
||||||
|
# [windows] find /q/music/albums/Phant*24bit -printf '%s %p\n' | sort -n | tail -n 8 | sed -r 's/^[0-9]+ //' | while IFS= read -r x; do dd if="$x" of=/dev/null bs=4k count=8192 & done
|
||||||
|
# [alpine] ll t; for x in t/2020_0724_16{2,3}*; do dd if="$x" of=/dev/null bs=4k count=10240 & done
|
||||||
|
#
|
||||||
|
# 72.4983 windows mintty msys2 fancy_log
|
||||||
|
# 219.5781 windows cmd msys2 fancy_log
|
||||||
|
# nope.avi windows cmd cpy3 fancy_log
|
||||||
|
# 9.8817 windows mintty msys2 RecentLog 200 50 0.1
|
||||||
|
# 10.2241 windows cmd cpy3 RecentLog 200 50 0.1
|
||||||
|
# 9.8494 windows cmd msys2 RecentLog 200 50 0.1
|
||||||
|
# 7.8061 windows mintty msys2 fancy_log <info-only>
|
||||||
|
# 7.9961 windows mintty msys2 RecentLog <info-only>
|
||||||
|
# 4.2603 alpine xfce4 cpy3 RecentLog
|
||||||
|
# 4.1538 alpine xfce4 cpy3 fancy_log
|
||||||
|
# 3.1742 alpine urxvt cpy3 fancy_log
|
||||||
|
|
||||||
|
|
||||||
def get_tid():
|
def get_tid():
|
||||||
return threading.current_thread().ident
|
return threading.current_thread().ident
|
||||||
|
|
||||||
|
|
||||||
|
def html_dec(txt):
|
||||||
|
return (
|
||||||
|
txt.replace("<", "<")
|
||||||
|
.replace(">", ">")
|
||||||
|
.replace(""", '"')
|
||||||
|
.replace(" ", "\r")
|
||||||
|
.replace(" ", "\n")
|
||||||
|
.replace("&", "&")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class CacheNode(object):
|
class CacheNode(object):
|
||||||
def __init__(self, tag, data):
|
def __init__(self, tag, data):
|
||||||
self.tag = tag
|
self.tag = tag
|
||||||
@@ -89,10 +225,11 @@ class CacheNode(object):
|
|||||||
|
|
||||||
|
|
||||||
class Gateway(object):
|
class Gateway(object):
|
||||||
def __init__(self, base_url):
|
def __init__(self, ar):
|
||||||
self.base_url = base_url
|
self.base_url = ar.base_url
|
||||||
|
self.password = ar.a
|
||||||
|
|
||||||
ui = urllib.parse.urlparse(base_url)
|
ui = urllib.parse.urlparse(self.base_url)
|
||||||
self.web_root = ui.path.strip("/")
|
self.web_root = ui.path.strip("/")
|
||||||
try:
|
try:
|
||||||
self.web_host, self.web_port = ui.netloc.split(":")
|
self.web_host, self.web_port = ui.netloc.split(":")
|
||||||
@@ -102,15 +239,25 @@ class Gateway(object):
|
|||||||
if ui.scheme == "http":
|
if ui.scheme == "http":
|
||||||
self.web_port = 80
|
self.web_port = 80
|
||||||
elif ui.scheme == "https":
|
elif ui.scheme == "https":
|
||||||
raise Exception("todo")
|
self.web_port = 443
|
||||||
else:
|
else:
|
||||||
raise Exception("bad url?")
|
raise Exception("bad url?")
|
||||||
|
|
||||||
|
self.ssl_context = None
|
||||||
|
self.use_tls = ui.scheme.lower() == "https"
|
||||||
|
if self.use_tls:
|
||||||
|
import ssl
|
||||||
|
|
||||||
|
if ar.td:
|
||||||
|
self.ssl_context = ssl._create_unverified_context()
|
||||||
|
elif ar.te:
|
||||||
|
self.ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS)
|
||||||
|
self.ssl_context.load_verify_locations(ar.te)
|
||||||
|
|
||||||
self.conns = {}
|
self.conns = {}
|
||||||
|
|
||||||
def quotep(self, path):
|
def quotep(self, path):
|
||||||
# TODO: mojibake support
|
path = path.encode("wtf-8")
|
||||||
path = path.encode("utf-8", "ignore")
|
|
||||||
return quote(path, safe="/")
|
return quote(path, safe="/")
|
||||||
|
|
||||||
def getconn(self, tid=None):
|
def getconn(self, tid=None):
|
||||||
@@ -118,9 +265,17 @@ class Gateway(object):
|
|||||||
try:
|
try:
|
||||||
return self.conns[tid]
|
return self.conns[tid]
|
||||||
except:
|
except:
|
||||||
log("new conn [{}] [{}]".format(self.web_host, self.web_port))
|
info("new conn [{}] [{}]".format(self.web_host, self.web_port))
|
||||||
|
|
||||||
conn = http.client.HTTPConnection(self.web_host, self.web_port, timeout=260)
|
args = {}
|
||||||
|
if not self.use_tls:
|
||||||
|
C = http.client.HTTPConnection
|
||||||
|
else:
|
||||||
|
C = http.client.HTTPSConnection
|
||||||
|
if self.ssl_context:
|
||||||
|
args = {"context": self.ssl_context}
|
||||||
|
|
||||||
|
conn = C(self.web_host, self.web_port, timeout=260, **args)
|
||||||
|
|
||||||
self.conns[tid] = conn
|
self.conns[tid] = conn
|
||||||
return conn
|
return conn
|
||||||
@@ -133,42 +288,75 @@ class Gateway(object):
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def sendreq(self, *args, **kwargs):
|
def sendreq(self, *args, headers={}, **kwargs):
|
||||||
tid = get_tid()
|
tid = get_tid()
|
||||||
|
if self.password:
|
||||||
|
headers["Cookie"] = "=".join(["cppwd", self.password])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
c = self.getconn(tid)
|
c = self.getconn(tid)
|
||||||
c.request(*list(args), **kwargs)
|
c.request(*list(args), headers=headers, **kwargs)
|
||||||
return c.getresponse()
|
return c.getresponse()
|
||||||
except:
|
except:
|
||||||
self.closeconn(tid)
|
dbg("bad conn")
|
||||||
|
|
||||||
|
self.closeconn(tid)
|
||||||
|
try:
|
||||||
c = self.getconn(tid)
|
c = self.getconn(tid)
|
||||||
c.request(*list(args), **kwargs)
|
c.request(*list(args), headers=headers, **kwargs)
|
||||||
return c.getresponse()
|
return c.getresponse()
|
||||||
|
except:
|
||||||
|
info("http connection failed:\n" + traceback.format_exc())
|
||||||
|
if self.use_tls and not self.ssl_context:
|
||||||
|
import ssl
|
||||||
|
|
||||||
|
cert = ssl.get_server_certificate((self.web_host, self.web_port))
|
||||||
|
info("server certificate probably not trusted:\n" + cert)
|
||||||
|
|
||||||
|
raise
|
||||||
|
|
||||||
def listdir(self, path):
|
def listdir(self, path):
|
||||||
web_path = "/" + "/".join([self.web_root, path])
|
if bad_good:
|
||||||
|
path = dewin(path)
|
||||||
|
|
||||||
r = self.sendreq("GET", self.quotep(web_path))
|
web_path = self.quotep("/" + "/".join([self.web_root, path])) + "?dots"
|
||||||
|
r = self.sendreq("GET", web_path)
|
||||||
if r.status != 200:
|
if r.status != 200:
|
||||||
self.closeconn()
|
self.closeconn()
|
||||||
raise Exception(
|
log(
|
||||||
"http error {} reading dir {} in {:x}".format(
|
"http error {} reading dir {} in {}".format(
|
||||||
r.status, web_path, rice_tid()
|
r.status, web_path, rice_tid()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
raise FuseOSError(errno.ENOENT)
|
||||||
|
|
||||||
return self.parse_html(r)
|
if not r.getheader("Content-Type", "").startswith("text/html"):
|
||||||
|
log("listdir on file: {}".format(path))
|
||||||
|
raise FuseOSError(errno.ENOENT)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return self.parse_html(r)
|
||||||
|
except:
|
||||||
|
info(repr(path) + "\n" + traceback.format_exc())
|
||||||
|
raise
|
||||||
|
|
||||||
def download_file_range(self, path, ofs1, ofs2):
|
def download_file_range(self, path, ofs1, ofs2):
|
||||||
web_path = "/" + "/".join([self.web_root, path])
|
if bad_good:
|
||||||
hdr_range = "bytes={}-{}".format(ofs1, ofs2)
|
path = dewin(path)
|
||||||
log("downloading {}".format(hdr_range))
|
|
||||||
|
|
||||||
r = self.sendreq("GET", self.quotep(web_path), headers={"Range": hdr_range})
|
web_path = self.quotep("/" + "/".join([self.web_root, path])) + "?raw"
|
||||||
|
hdr_range = "bytes={}-{}".format(ofs1, ofs2 - 1)
|
||||||
|
info(
|
||||||
|
"DL {:4.0f}K\033[36m{:>9}-{:<9}\033[0m{}".format(
|
||||||
|
(ofs2 - ofs1) / 1024.0, ofs1, ofs2 - 1, hexler(path)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
r = self.sendreq("GET", web_path, headers={"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(
|
||||||
"http error {} reading file {} range {} in {:x}".format(
|
"http error {} reading file {} range {} in {}".format(
|
||||||
r.status, web_path, hdr_range, rice_tid()
|
r.status, web_path, hdr_range, rice_tid()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -179,7 +367,7 @@ class Gateway(object):
|
|||||||
ret = []
|
ret = []
|
||||||
remainder = b""
|
remainder = b""
|
||||||
ptn = re.compile(
|
ptn = re.compile(
|
||||||
r"^<tr><td>(-|DIR)</td><td><a [^>]+>([^<]+)</a></td><td>([^<]+)</td><td>([^<]+)</td></tr>$"
|
r'^<tr><td>(-|DIR)</td><td><a[^>]* href="([^"]+)"[^>]*>([^<]+)</a></td><td>([^<]+)</td><td>([^<]+)</td></tr>$'
|
||||||
)
|
)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
@@ -201,9 +389,22 @@ class Gateway(object):
|
|||||||
# print(line)
|
# print(line)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
ftype, fname, fsize, fdate = m.groups()
|
ftype, furl, fname, fsize, fdate = m.groups()
|
||||||
ts = datetime.strptime(fdate, "%Y-%m-%d %H:%M:%S").timestamp()
|
fname = furl.rstrip("/").split("/")[-1]
|
||||||
sz = int(fsize)
|
fname = unquote(fname)
|
||||||
|
fname = fname.decode("wtf-8")
|
||||||
|
if bad_good:
|
||||||
|
fname = enwin(fname)
|
||||||
|
|
||||||
|
sz = 1
|
||||||
|
ts = 60 * 60 * 24 * 2
|
||||||
|
try:
|
||||||
|
sz = int(fsize)
|
||||||
|
ts = datetime.strptime(fdate, "%Y-%m-%d %H:%M:%S").timestamp()
|
||||||
|
except:
|
||||||
|
info("bad HTML or OS [{}] [{}]".format(fdate, fsize))
|
||||||
|
# python cannot strptime(1959-01-01) on windows
|
||||||
|
|
||||||
if ftype == "-":
|
if ftype == "-":
|
||||||
ret.append([fname, self.stat_file(ts, sz), 0])
|
ret.append([fname, self.stat_file(ts, sz), 0])
|
||||||
else:
|
else:
|
||||||
@@ -213,7 +414,7 @@ class Gateway(object):
|
|||||||
|
|
||||||
def stat_dir(self, ts, sz=4096):
|
def stat_dir(self, ts, sz=4096):
|
||||||
return {
|
return {
|
||||||
"st_mode": 0o555 | stat.S_IFDIR,
|
"st_mode": stat.S_IFDIR | 0o555,
|
||||||
"st_uid": 1000,
|
"st_uid": 1000,
|
||||||
"st_gid": 1000,
|
"st_gid": 1000,
|
||||||
"st_size": sz,
|
"st_size": sz,
|
||||||
@@ -225,7 +426,7 @@ class Gateway(object):
|
|||||||
|
|
||||||
def stat_file(self, ts, sz):
|
def stat_file(self, ts, sz):
|
||||||
return {
|
return {
|
||||||
"st_mode": 0o444 | stat.S_IFREG,
|
"st_mode": stat.S_IFREG | 0o444,
|
||||||
"st_uid": 1000,
|
"st_uid": 1000,
|
||||||
"st_gid": 1000,
|
"st_gid": 1000,
|
||||||
"st_size": sz,
|
"st_size": sz,
|
||||||
@@ -237,8 +438,11 @@ class Gateway(object):
|
|||||||
|
|
||||||
|
|
||||||
class CPPF(Operations):
|
class CPPF(Operations):
|
||||||
def __init__(self, base_url):
|
def __init__(self, ar):
|
||||||
self.gw = Gateway(base_url)
|
self.gw = Gateway(ar)
|
||||||
|
self.junk_fh_ctr = 3
|
||||||
|
self.n_dircache = ar.cd
|
||||||
|
self.n_filecache = ar.cf
|
||||||
|
|
||||||
self.dircache = []
|
self.dircache = []
|
||||||
self.dircache_mtx = threading.Lock()
|
self.dircache_mtx = threading.Lock()
|
||||||
@@ -246,14 +450,29 @@ class CPPF(Operations):
|
|||||||
self.filecache = []
|
self.filecache = []
|
||||||
self.filecache_mtx = threading.Lock()
|
self.filecache_mtx = threading.Lock()
|
||||||
|
|
||||||
log("up")
|
info("up")
|
||||||
|
|
||||||
|
def _describe(self):
|
||||||
|
msg = ""
|
||||||
|
with self.filecache_mtx:
|
||||||
|
for n, cn in enumerate(self.filecache):
|
||||||
|
cache_path, cache1 = cn.tag
|
||||||
|
cache2 = cache1 + len(cn.data)
|
||||||
|
msg += "\n{:<2} {:>7} {:>10}:{:<9} {}".format(
|
||||||
|
n,
|
||||||
|
len(cn.data),
|
||||||
|
cache1,
|
||||||
|
cache2,
|
||||||
|
cache_path.replace("\r", "\\r").replace("\n", "\\n"),
|
||||||
|
)
|
||||||
|
return msg
|
||||||
|
|
||||||
def clean_dircache(self):
|
def clean_dircache(self):
|
||||||
"""not threadsafe"""
|
"""not threadsafe"""
|
||||||
now = time.time()
|
now = time.time()
|
||||||
cutoff = 0
|
cutoff = 0
|
||||||
for cn in self.dircache:
|
for cn in self.dircache:
|
||||||
if cn.ts - now > 1:
|
if now - cn.ts > self.n_dircache:
|
||||||
cutoff += 1
|
cutoff += 1
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
@@ -262,8 +481,7 @@ class CPPF(Operations):
|
|||||||
self.dircache = self.dircache[cutoff:]
|
self.dircache = self.dircache[cutoff:]
|
||||||
|
|
||||||
def get_cached_dir(self, dirpath):
|
def get_cached_dir(self, dirpath):
|
||||||
# with self.dircache_mtx:
|
with self.dircache_mtx:
|
||||||
if True:
|
|
||||||
self.clean_dircache()
|
self.clean_dircache()
|
||||||
for cn in self.dircache:
|
for cn in self.dircache:
|
||||||
if cn.tag == dirpath:
|
if cn.tag == dirpath:
|
||||||
@@ -300,9 +518,8 @@ class CPPF(Operations):
|
|||||||
car = None
|
car = None
|
||||||
cdr = None
|
cdr = None
|
||||||
ncn = -1
|
ncn = -1
|
||||||
# with self.filecache_mtx:
|
dbg("cache request {}:{} |{}|".format(get1, get2, file_sz) + self._describe())
|
||||||
if True:
|
with self.filecache_mtx:
|
||||||
dbg("cache request from {} to {}, size {}".format(get1, get2, file_sz))
|
|
||||||
for cn in self.filecache:
|
for cn in self.filecache:
|
||||||
ncn += 1
|
ncn += 1
|
||||||
|
|
||||||
@@ -312,6 +529,12 @@ class CPPF(Operations):
|
|||||||
|
|
||||||
cache2 = cache1 + len(cn.data)
|
cache2 = cache1 + len(cn.data)
|
||||||
if get2 <= cache1 or get1 >= cache2:
|
if get2 <= cache1 or get1 >= cache2:
|
||||||
|
# request does not overlap with cached area at all
|
||||||
|
continue
|
||||||
|
|
||||||
|
if get1 < cache1 and get2 > cache2:
|
||||||
|
# cached area does overlap, but must specifically contain
|
||||||
|
# either the first or last byte in the requested range
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if get1 >= cache1 and get2 <= cache2:
|
if get1 >= cache1 and get2 <= cache2:
|
||||||
@@ -322,7 +545,7 @@ class CPPF(Operations):
|
|||||||
buf_ofs = get1 - cache1
|
buf_ofs = get1 - cache1
|
||||||
buf_end = buf_ofs + (get2 - get1)
|
buf_end = buf_ofs + (get2 - get1)
|
||||||
dbg(
|
dbg(
|
||||||
"found all ({}, {} to {}, len {}) [{}:{}] = {}".format(
|
"found all (#{} {}:{} |{}|) [{}:{}] = {}".format(
|
||||||
ncn,
|
ncn,
|
||||||
cache1,
|
cache1,
|
||||||
cache2,
|
cache2,
|
||||||
@@ -334,11 +557,11 @@ class CPPF(Operations):
|
|||||||
)
|
)
|
||||||
return cn.data[buf_ofs:buf_end]
|
return cn.data[buf_ofs:buf_end]
|
||||||
|
|
||||||
if get2 < cache2:
|
if get2 <= cache2:
|
||||||
x = cn.data[: get2 - cache1]
|
x = cn.data[: get2 - cache1]
|
||||||
if not cdr or len(cdr) < len(x):
|
if not cdr or len(cdr) < len(x):
|
||||||
dbg(
|
dbg(
|
||||||
"found car ({}, {} to {}, len {}) [:{}-{}] = [:{}] = {}".format(
|
"found cdr (#{} {}:{} |{}|) [:{}-{}] = [:{}] = {}".format(
|
||||||
ncn,
|
ncn,
|
||||||
cache1,
|
cache1,
|
||||||
cache2,
|
cache2,
|
||||||
@@ -353,11 +576,11 @@ class CPPF(Operations):
|
|||||||
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if get1 > cache1:
|
if get1 >= cache1:
|
||||||
x = cn.data[-(cache2 - get1) :]
|
x = cn.data[-(max(0, cache2 - get1)) :]
|
||||||
if not car or len(car) < len(x):
|
if not car or len(car) < len(x):
|
||||||
dbg(
|
dbg(
|
||||||
"found cdr ({}, {} to {}, len {}) [-({}-{}):] = [-{}:] = {}".format(
|
"found car (#{} {}:{} |{}|) [-({}-{}):] = [-{}:] = {}".format(
|
||||||
ncn,
|
ncn,
|
||||||
cache1,
|
cache1,
|
||||||
cache2,
|
cache2,
|
||||||
@@ -372,38 +595,52 @@ class CPPF(Operations):
|
|||||||
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
raise Exception("what")
|
msg = "cache fallthrough\n{} {} {}\n{} {} {}\n{} {} --\n".format(
|
||||||
|
get1,
|
||||||
|
get2,
|
||||||
|
get2 - get1,
|
||||||
|
cache1,
|
||||||
|
cache2,
|
||||||
|
cache2 - cache1,
|
||||||
|
get1 - cache1,
|
||||||
|
get2 - cache2,
|
||||||
|
)
|
||||||
|
msg += self._describe()
|
||||||
|
raise Exception(msg)
|
||||||
|
|
||||||
if car and cdr:
|
if car and cdr and len(car) + len(cdr) == get2 - get1:
|
||||||
dbg("<cache> have both")
|
dbg("<cache> have both")
|
||||||
|
return car + cdr
|
||||||
|
|
||||||
ret = car + cdr
|
elif cdr and (not car or len(car) < len(cdr)):
|
||||||
if len(ret) == get2 - get1:
|
|
||||||
return ret
|
|
||||||
|
|
||||||
raise Exception("{} + {} != {} - {}".format(len(car), len(cdr), get2, get1))
|
|
||||||
|
|
||||||
elif cdr:
|
|
||||||
h_end = get1 + (get2 - get1) - len(cdr)
|
h_end = get1 + (get2 - get1) - len(cdr)
|
||||||
h_ofs = h_end - 512 * 1024
|
h_ofs = min(get1, h_end - 512 * 1024)
|
||||||
|
|
||||||
if h_ofs < 0:
|
if h_ofs < 0:
|
||||||
h_ofs = 0
|
h_ofs = 0
|
||||||
|
|
||||||
buf_ofs = (get2 - get1) - len(cdr)
|
buf_ofs = get1 - h_ofs
|
||||||
|
|
||||||
dbg(
|
dbg(
|
||||||
"<cache> cdr {}, car {}-{}={} [-{}:]".format(
|
"<cache> cdr {}, car {}:{} |{}| [{}:]".format(
|
||||||
len(cdr), h_ofs, h_end, h_end - h_ofs, buf_ofs
|
len(cdr), h_ofs, h_end, h_end - h_ofs, buf_ofs
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
buf = self.gw.download_file_range(path, h_ofs, h_end - 1)
|
buf = self.gw.download_file_range(path, h_ofs, h_end)
|
||||||
ret = buf[-buf_ofs:] + cdr
|
if len(buf) == h_end - h_ofs:
|
||||||
|
ret = buf[buf_ofs:] + cdr
|
||||||
|
else:
|
||||||
|
ret = buf[get1 - h_ofs :]
|
||||||
|
info(
|
||||||
|
"remote truncated {}:{} to |{}|, will return |{}|".format(
|
||||||
|
h_ofs, h_end, len(buf), len(ret)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
elif car:
|
elif car:
|
||||||
h_ofs = get1 + len(car)
|
h_ofs = get1 + len(car)
|
||||||
h_end = h_ofs + 1024 * 1024
|
h_end = max(get2, h_ofs + 1024 * 1024)
|
||||||
|
|
||||||
if h_end > file_sz:
|
if h_end > file_sz:
|
||||||
h_end = file_sz
|
h_end = file_sz
|
||||||
@@ -411,17 +648,22 @@ class CPPF(Operations):
|
|||||||
buf_ofs = (get2 - get1) - len(car)
|
buf_ofs = (get2 - get1) - len(car)
|
||||||
|
|
||||||
dbg(
|
dbg(
|
||||||
"<cache> car {}, cdr {}-{}={} [:{}]".format(
|
"<cache> car {}, cdr {}:{} |{}| [:{}]".format(
|
||||||
len(car), h_ofs, h_end, h_end - h_ofs, buf_ofs
|
len(car), h_ofs, h_end, h_end - h_ofs, buf_ofs
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
buf = self.gw.download_file_range(path, h_ofs, h_end - 1)
|
buf = self.gw.download_file_range(path, h_ofs, h_end)
|
||||||
ret = car + buf[:buf_ofs]
|
ret = car + buf[:buf_ofs]
|
||||||
|
|
||||||
else:
|
else:
|
||||||
h_ofs = get1 - 256 * 1024
|
if get2 - get1 <= 1024 * 1024:
|
||||||
h_end = get2 + 1024 * 1024
|
h_ofs = get1 - 256 * 1024
|
||||||
|
h_end = get2 + 1024 * 1024
|
||||||
|
else:
|
||||||
|
# big enough, doesn't need pads
|
||||||
|
h_ofs = get1
|
||||||
|
h_end = get2
|
||||||
|
|
||||||
if h_ofs < 0:
|
if h_ofs < 0:
|
||||||
h_ofs = 0
|
h_ofs = 0
|
||||||
@@ -433,54 +675,99 @@ class CPPF(Operations):
|
|||||||
buf_end = buf_ofs + get2 - get1
|
buf_end = buf_ofs + get2 - get1
|
||||||
|
|
||||||
dbg(
|
dbg(
|
||||||
"<cache> {}-{}={} [{}:{}]".format(
|
"<cache> {}:{} |{}| [{}:{}]".format(
|
||||||
h_ofs, h_end, h_end - h_ofs, buf_ofs, buf_end
|
h_ofs, h_end, h_end - h_ofs, buf_ofs, buf_end
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
buf = self.gw.download_file_range(path, h_ofs, h_end - 1)
|
buf = self.gw.download_file_range(path, h_ofs, h_end)
|
||||||
ret = buf[buf_ofs:buf_end]
|
ret = buf[buf_ofs:buf_end]
|
||||||
|
|
||||||
cn = CacheNode([path, h_ofs], buf)
|
cn = CacheNode([path, h_ofs], buf)
|
||||||
# with self.filecache_mtx:
|
with self.filecache_mtx:
|
||||||
if True:
|
if len(self.filecache) >= self.n_filecache:
|
||||||
if len(self.filecache) > 6:
|
|
||||||
self.filecache = self.filecache[1:] + [cn]
|
self.filecache = self.filecache[1:] + [cn]
|
||||||
else:
|
else:
|
||||||
self.filecache.append(cn)
|
self.filecache.append(cn)
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def readdir(self, path, fh=None):
|
def _readdir(self, path, fh=None):
|
||||||
path = path.strip("/")
|
path = path.strip("/")
|
||||||
log("readdir {}".format(path))
|
log("readdir [{}] [{}]".format(hexler(path), fh))
|
||||||
|
|
||||||
ret = self.gw.listdir(path)
|
ret = self.gw.listdir(path)
|
||||||
|
if not self.n_dircache:
|
||||||
|
return ret
|
||||||
|
|
||||||
# with self.dircache_mtx:
|
with self.dircache_mtx:
|
||||||
if True:
|
|
||||||
cn = CacheNode(path, ret)
|
cn = CacheNode(path, ret)
|
||||||
self.dircache.append(cn)
|
self.dircache.append(cn)
|
||||||
self.clean_dircache()
|
self.clean_dircache()
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
def readdir(self, path, fh=None):
|
||||||
|
return [".", ".."] + self._readdir(path, fh)
|
||||||
|
|
||||||
def read(self, path, length, offset, fh=None):
|
def read(self, path, length, offset, fh=None):
|
||||||
|
req_max = 1024 * 1024 * 8
|
||||||
|
cache_max = 1024 * 1024 * 2
|
||||||
|
if length > req_max:
|
||||||
|
# windows actually doing 240 MiB read calls, sausage
|
||||||
|
info("truncate |{}| to {}MiB".format(length, req_max >> 20))
|
||||||
|
length = req_max
|
||||||
|
|
||||||
path = path.strip("/")
|
path = path.strip("/")
|
||||||
|
|
||||||
ofs2 = offset + length
|
ofs2 = offset + length
|
||||||
log("read {} @ {} len {} end {}".format(path, offset, length, ofs2))
|
|
||||||
|
|
||||||
file_sz = self.getattr(path)["st_size"]
|
file_sz = self.getattr(path)["st_size"]
|
||||||
if ofs2 >= file_sz:
|
log(
|
||||||
ofs2 = file_sz - 1
|
"read {} |{}| {}:{} max {}".format(
|
||||||
log("truncate to len {} end {}".format((ofs2 - offset) + 1, ofs2))
|
hexler(path), length, offset, ofs2, file_sz
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if ofs2 > file_sz:
|
||||||
|
ofs2 = file_sz
|
||||||
|
log("truncate to |{}| :{}".format(ofs2 - offset, ofs2))
|
||||||
|
|
||||||
# toggle cache here i suppose
|
if file_sz == 0 or offset >= ofs2:
|
||||||
# return self.get_cached_file(path, offset, ofs2, file_sz)
|
return b""
|
||||||
return self.gw.download_file_range(path, offset, ofs2 - 1)
|
|
||||||
|
if self.n_filecache and length <= cache_max:
|
||||||
|
ret = self.get_cached_file(path, offset, ofs2, file_sz)
|
||||||
|
else:
|
||||||
|
ret = self.gw.download_file_range(path, offset, ofs2)
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
fn = "cppf-{}-{}-{}".format(time.time(), offset, length)
|
||||||
|
if False:
|
||||||
|
with open(fn, "wb", len(ret)) as f:
|
||||||
|
f.write(ret)
|
||||||
|
elif self.n_filecache:
|
||||||
|
ret2 = self.gw.download_file_range(path, offset, ofs2)
|
||||||
|
if ret != ret2:
|
||||||
|
info(fn)
|
||||||
|
for v in [ret, ret2]:
|
||||||
|
try:
|
||||||
|
info(len(v))
|
||||||
|
except:
|
||||||
|
info("uhh " + repr(v))
|
||||||
|
|
||||||
|
with open(fn + ".bad", "wb") as f:
|
||||||
|
f.write(ret)
|
||||||
|
with open(fn + ".good", "wb") as f:
|
||||||
|
f.write(ret2)
|
||||||
|
|
||||||
|
raise Exception("cache bork")
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
def getattr(self, path, fh=None):
|
def getattr(self, path, fh=None):
|
||||||
|
log("getattr [{}]".format(hexler(path)))
|
||||||
|
if WINDOWS:
|
||||||
|
path = enwin(path) # windows occasionally decodes f0xx to xx
|
||||||
|
|
||||||
path = path.strip("/")
|
path = path.strip("/")
|
||||||
try:
|
try:
|
||||||
dirpath, fname = path.rsplit("/", 1)
|
dirpath, fname = path.rsplit("/", 1)
|
||||||
@@ -488,23 +775,34 @@ class CPPF(Operations):
|
|||||||
dirpath = ""
|
dirpath = ""
|
||||||
fname = path
|
fname = path
|
||||||
|
|
||||||
log("getattr {}".format(path))
|
|
||||||
|
|
||||||
if not path:
|
if not path:
|
||||||
return self.gw.stat_dir(time.time())
|
ret = self.gw.stat_dir(time.time())
|
||||||
|
# dbg("=" + repr(ret))
|
||||||
|
return ret
|
||||||
|
|
||||||
cn = self.get_cached_dir(dirpath)
|
cn = self.get_cached_dir(dirpath)
|
||||||
if cn:
|
if cn:
|
||||||
# log('cache ok')
|
log("cache ok")
|
||||||
dents = cn.data
|
dents = cn.data
|
||||||
else:
|
else:
|
||||||
log("cache miss")
|
dbg("cache miss")
|
||||||
dents = self.readdir(dirpath)
|
dents = self._readdir(dirpath)
|
||||||
|
|
||||||
for cache_name, cache_stat, _ in dents:
|
for cache_name, cache_stat, _ in dents:
|
||||||
|
# if "qw" in cache_name and "qw" in fname:
|
||||||
|
# info(
|
||||||
|
# "cmp\n [{}]\n [{}]\n\n{}\n".format(
|
||||||
|
# hexler(cache_name),
|
||||||
|
# hexler(fname),
|
||||||
|
# "\n".join(traceback.format_stack()[:-1]),
|
||||||
|
# )
|
||||||
|
# )
|
||||||
|
|
||||||
if cache_name == fname:
|
if cache_name == fname:
|
||||||
|
# dbg("=" + repr(cache_stat))
|
||||||
return cache_stat
|
return cache_stat
|
||||||
|
|
||||||
|
info("=ENOENT ({})".format(hexler(path)))
|
||||||
raise FuseOSError(errno.ENOENT)
|
raise FuseOSError(errno.ENOENT)
|
||||||
|
|
||||||
access = None
|
access = None
|
||||||
@@ -517,17 +815,178 @@ class CPPF(Operations):
|
|||||||
releasedir = None
|
releasedir = None
|
||||||
statfs = None
|
statfs = None
|
||||||
|
|
||||||
|
if False:
|
||||||
|
# incorrect semantics but good for debugging stuff like samba and msys2
|
||||||
|
def access(self, path, mode):
|
||||||
|
log("@@ access [{}] [{}]".format(path, mode))
|
||||||
|
return 1 if self.getattr(path) else 0
|
||||||
|
|
||||||
|
def flush(self, path, fh):
|
||||||
|
log("@@ flush [{}] [{}]".format(path, fh))
|
||||||
|
return True
|
||||||
|
|
||||||
|
def getxattr(self, *args):
|
||||||
|
log("@@ getxattr [{}]".format("] [".join(str(x) for x in args)))
|
||||||
|
return False
|
||||||
|
|
||||||
|
def listxattr(self, *args):
|
||||||
|
log("@@ listxattr [{}]".format("] [".join(str(x) for x in args)))
|
||||||
|
return False
|
||||||
|
|
||||||
|
def open(self, path, flags):
|
||||||
|
log("@@ open [{}] [{}]".format(path, flags))
|
||||||
|
return 42
|
||||||
|
|
||||||
|
def opendir(self, fh):
|
||||||
|
log("@@ opendir [{}]".format(fh))
|
||||||
|
return 69
|
||||||
|
|
||||||
|
def release(self, ino, fi):
|
||||||
|
log("@@ release [{}] [{}]".format(ino, fi))
|
||||||
|
return True
|
||||||
|
|
||||||
|
def releasedir(self, ino, fi):
|
||||||
|
log("@@ releasedir [{}] [{}]".format(ino, fi))
|
||||||
|
return True
|
||||||
|
|
||||||
|
def statfs(self, path):
|
||||||
|
log("@@ statfs [{}]".format(path))
|
||||||
|
return {}
|
||||||
|
|
||||||
|
if sys.platform == "win32":
|
||||||
|
# quick compat for /mingw64/bin/python3 (msys2)
|
||||||
|
def _open(self, path):
|
||||||
|
try:
|
||||||
|
x = self.getattr(path)
|
||||||
|
if x["st_mode"] <= 0:
|
||||||
|
raise Exception()
|
||||||
|
|
||||||
|
self.junk_fh_ctr += 1
|
||||||
|
if self.junk_fh_ctr > 32000: # TODO untested
|
||||||
|
self.junk_fh_ctr = 4
|
||||||
|
|
||||||
|
return self.junk_fh_ctr
|
||||||
|
|
||||||
|
except Exception as ex:
|
||||||
|
log("open ERR {}".format(repr(ex)))
|
||||||
|
raise FuseOSError(errno.ENOENT)
|
||||||
|
|
||||||
|
def open(self, path, flags):
|
||||||
|
dbg("open [{}] [{}]".format(hexler(path), flags))
|
||||||
|
return self._open(path)
|
||||||
|
|
||||||
|
def opendir(self, path):
|
||||||
|
dbg("opendir [{}]".format(hexler(path)))
|
||||||
|
return self._open(path)
|
||||||
|
|
||||||
|
def flush(self, path, fh):
|
||||||
|
dbg("flush [{}] [{}]".format(hexler(path), fh))
|
||||||
|
|
||||||
|
def release(self, ino, fi):
|
||||||
|
dbg("release [{}] [{}]".format(hexler(ino), fi))
|
||||||
|
|
||||||
|
def releasedir(self, ino, fi):
|
||||||
|
dbg("releasedir [{}] [{}]".format(hexler(ino), fi))
|
||||||
|
|
||||||
|
def access(self, path, mode):
|
||||||
|
dbg("access [{}] [{}]".format(hexler(path), mode))
|
||||||
|
try:
|
||||||
|
x = self.getattr(path)
|
||||||
|
if x["st_mode"] <= 0:
|
||||||
|
raise Exception()
|
||||||
|
except:
|
||||||
|
raise FuseOSError(errno.ENOENT)
|
||||||
|
|
||||||
|
|
||||||
|
class TheArgparseFormatter(
|
||||||
|
argparse.RawTextHelpFormatter, argparse.ArgumentDefaultsHelpFormatter
|
||||||
|
):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
try:
|
global info, log, dbg
|
||||||
local, remote = sys.argv[1:]
|
|
||||||
except:
|
|
||||||
print("need arg 1: local directory")
|
|
||||||
print("need arg 2: root url")
|
|
||||||
return
|
|
||||||
|
|
||||||
FUSE(CPPF(remote), local, foreground=True, nothreads=True)
|
# filecache helps for reads that are ~64k or smaller;
|
||||||
# if nothreads=False also uncomment the `with *_mtx` things
|
# linux generally does 128k so the cache is a slowdown,
|
||||||
|
# windows likes to use 4k and 64k so cache is required,
|
||||||
|
# value is numChunks (1~3M each) to keep in the cache
|
||||||
|
nf = 24
|
||||||
|
|
||||||
|
# dircache is always a boost,
|
||||||
|
# only want to disable it for tests etc,
|
||||||
|
# value is numSec until an entry goes stale
|
||||||
|
nd = 1
|
||||||
|
|
||||||
|
where = "local directory"
|
||||||
|
if WINDOWS:
|
||||||
|
where += " or DRIVE:"
|
||||||
|
|
||||||
|
ex_pre = "\n " + os.path.basename(__file__) + " "
|
||||||
|
examples = ["http://192.168.1.69:3923/music/ ./music"]
|
||||||
|
if WINDOWS:
|
||||||
|
examples.append("http://192.168.1.69:3923/music/ M:")
|
||||||
|
|
||||||
|
ap = argparse.ArgumentParser(
|
||||||
|
formatter_class=TheArgparseFormatter,
|
||||||
|
epilog="example:" + ex_pre + ex_pre.join(examples),
|
||||||
|
)
|
||||||
|
ap.add_argument(
|
||||||
|
"-cd", metavar="NUM_SECONDS", type=float, default=nd, help="directory cache"
|
||||||
|
)
|
||||||
|
ap.add_argument(
|
||||||
|
"-cf", metavar="NUM_BLOCKS", type=int, default=nf, help="file cache"
|
||||||
|
)
|
||||||
|
ap.add_argument("-a", metavar="PASSWORD", help="password")
|
||||||
|
ap.add_argument("-d", action="store_true", help="enable debug")
|
||||||
|
ap.add_argument("-te", metavar="PEM_FILE", help="certificate to expect/verify")
|
||||||
|
ap.add_argument("-td", action="store_true", help="disable certificate check")
|
||||||
|
ap.add_argument("base_url", type=str, help="remote copyparty URL to mount")
|
||||||
|
ap.add_argument("local_path", type=str, help=where + " to mount it on")
|
||||||
|
ar = ap.parse_args()
|
||||||
|
|
||||||
|
if ar.d:
|
||||||
|
# windows terminals are slow (cmd.exe, mintty)
|
||||||
|
# otoh fancy_log beats RecentLog on linux
|
||||||
|
logger = RecentLog().put if WINDOWS else fancy_log
|
||||||
|
|
||||||
|
info = logger
|
||||||
|
log = logger
|
||||||
|
dbg = logger
|
||||||
|
else:
|
||||||
|
# debug=off, speed is dontcare
|
||||||
|
info = fancy_log
|
||||||
|
log = null_log
|
||||||
|
dbg = null_log
|
||||||
|
|
||||||
|
if WINDOWS:
|
||||||
|
os.system("")
|
||||||
|
|
||||||
|
for ch in '<>:"\\|?*':
|
||||||
|
# microsoft maps illegal characters to f0xx
|
||||||
|
# (e000 to f8ff is basic-plane private-use)
|
||||||
|
bad_good[ch] = chr(ord(ch) + 0xF000)
|
||||||
|
|
||||||
|
for n in range(0, 0x100):
|
||||||
|
# map surrogateescape to another private-use area
|
||||||
|
bad_good[chr(n + 0xDC00)] = chr(n + 0xF100)
|
||||||
|
|
||||||
|
for k, v in bad_good.items():
|
||||||
|
good_bad[v] = k
|
||||||
|
|
||||||
|
register_wtf8()
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open("/etc/fuse.conf", "rb") as f:
|
||||||
|
allow_other = b"\nuser_allow_other" in f.read()
|
||||||
|
except:
|
||||||
|
allow_other = WINDOWS or MACOS
|
||||||
|
|
||||||
|
args = {"foreground": True, "nothreads": True, "allow_other": allow_other}
|
||||||
|
if not MACOS:
|
||||||
|
args["nonempty"] = True
|
||||||
|
|
||||||
|
FUSE(CPPF(ar), ar.local_path, encoding="wtf-8", **args)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
590
bin/copyparty-fuseb.py
Executable file
590
bin/copyparty-fuseb.py
Executable file
@@ -0,0 +1,590 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
from __future__ import print_function, unicode_literals
|
||||||
|
|
||||||
|
"""copyparty-fuseb: remote copyparty as a local filesystem"""
|
||||||
|
__author__ = "ed <copyparty@ocv.me>"
|
||||||
|
__copyright__ = 2020
|
||||||
|
__license__ = "MIT"
|
||||||
|
__url__ = "https://github.com/9001/copyparty/"
|
||||||
|
|
||||||
|
import re
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import stat
|
||||||
|
import errno
|
||||||
|
import struct
|
||||||
|
import threading
|
||||||
|
import http.client # py2: httplib
|
||||||
|
import urllib.parse
|
||||||
|
from datetime import datetime
|
||||||
|
from urllib.parse import quote_from_bytes as quote
|
||||||
|
|
||||||
|
try:
|
||||||
|
import fuse
|
||||||
|
from fuse import Fuse
|
||||||
|
|
||||||
|
fuse.fuse_python_api = (0, 2)
|
||||||
|
if not hasattr(fuse, "__version__"):
|
||||||
|
raise Exception("your fuse-python is way old")
|
||||||
|
except:
|
||||||
|
print(
|
||||||
|
"\n could not import fuse; these may help:\n python3 -m pip install --user fuse-python\n apt install libfuse\n modprobe fuse\n"
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
mount a copyparty server (local or remote) as a filesystem
|
||||||
|
|
||||||
|
usage:
|
||||||
|
python ./copyparty-fuseb.py -f -o allow_other,auto_unmount,nonempty,url=http://192.168.1.69:3923 /mnt/nas
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
sudo apk add fuse-dev python3-dev
|
||||||
|
python3 -m pip install --user fuse-python
|
||||||
|
|
||||||
|
fork of copyparty-fuse.py based on fuse-python which
|
||||||
|
appears to be more compliant than fusepy? since this works with samba
|
||||||
|
(probably just my garbage code tbh)
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def threadless_log(msg):
|
||||||
|
print(msg + "\n", end="")
|
||||||
|
|
||||||
|
|
||||||
|
def boring_log(msg):
|
||||||
|
msg = "\033[36m{:012x}\033[0m {}\n".format(threading.current_thread().ident, msg)
|
||||||
|
print(msg[4:], end="")
|
||||||
|
|
||||||
|
|
||||||
|
def rice_tid():
|
||||||
|
tid = threading.current_thread().ident
|
||||||
|
c = struct.unpack(b"B" * 5, struct.pack(b">Q", tid)[-5:])
|
||||||
|
return "".join("\033[1;37;48;5;{}m{:02x}".format(x, x) for x in c) + "\033[0m"
|
||||||
|
|
||||||
|
|
||||||
|
def fancy_log(msg):
|
||||||
|
print("{} {}\n".format(rice_tid(), msg), end="")
|
||||||
|
|
||||||
|
|
||||||
|
def null_log(msg):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
info = fancy_log
|
||||||
|
log = fancy_log
|
||||||
|
dbg = fancy_log
|
||||||
|
log = null_log
|
||||||
|
dbg = null_log
|
||||||
|
|
||||||
|
|
||||||
|
def get_tid():
|
||||||
|
return threading.current_thread().ident
|
||||||
|
|
||||||
|
|
||||||
|
def html_dec(txt):
|
||||||
|
return (
|
||||||
|
txt.replace("<", "<")
|
||||||
|
.replace(">", ">")
|
||||||
|
.replace(""", '"')
|
||||||
|
.replace("&", "&")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CacheNode(object):
|
||||||
|
def __init__(self, tag, data):
|
||||||
|
self.tag = tag
|
||||||
|
self.data = data
|
||||||
|
self.ts = time.time()
|
||||||
|
|
||||||
|
|
||||||
|
class Stat(fuse.Stat):
|
||||||
|
def __init__(self):
|
||||||
|
self.st_mode = 0
|
||||||
|
self.st_ino = 0
|
||||||
|
self.st_dev = 0
|
||||||
|
self.st_nlink = 1
|
||||||
|
self.st_uid = 1000
|
||||||
|
self.st_gid = 1000
|
||||||
|
self.st_size = 0
|
||||||
|
self.st_atime = 0
|
||||||
|
self.st_mtime = 0
|
||||||
|
self.st_ctime = 0
|
||||||
|
|
||||||
|
|
||||||
|
class Gateway(object):
|
||||||
|
def __init__(self, base_url):
|
||||||
|
self.base_url = base_url
|
||||||
|
|
||||||
|
ui = urllib.parse.urlparse(base_url)
|
||||||
|
self.web_root = ui.path.strip("/")
|
||||||
|
try:
|
||||||
|
self.web_host, self.web_port = ui.netloc.split(":")
|
||||||
|
self.web_port = int(self.web_port)
|
||||||
|
except:
|
||||||
|
self.web_host = ui.netloc
|
||||||
|
if ui.scheme == "http":
|
||||||
|
self.web_port = 80
|
||||||
|
elif ui.scheme == "https":
|
||||||
|
raise Exception("todo")
|
||||||
|
else:
|
||||||
|
raise Exception("bad url?")
|
||||||
|
|
||||||
|
self.conns = {}
|
||||||
|
|
||||||
|
def quotep(self, path):
|
||||||
|
# TODO: mojibake support
|
||||||
|
path = path.encode("utf-8", "ignore")
|
||||||
|
return quote(path, safe="/")
|
||||||
|
|
||||||
|
def getconn(self, tid=None):
|
||||||
|
tid = tid or get_tid()
|
||||||
|
try:
|
||||||
|
return self.conns[tid]
|
||||||
|
except:
|
||||||
|
info("new conn [{}] [{}]".format(self.web_host, self.web_port))
|
||||||
|
|
||||||
|
conn = http.client.HTTPConnection(self.web_host, self.web_port, timeout=260)
|
||||||
|
|
||||||
|
self.conns[tid] = conn
|
||||||
|
return conn
|
||||||
|
|
||||||
|
def closeconn(self, tid=None):
|
||||||
|
tid = tid or get_tid()
|
||||||
|
try:
|
||||||
|
self.conns[tid].close()
|
||||||
|
del self.conns[tid]
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def sendreq(self, *args, **kwargs):
|
||||||
|
tid = get_tid()
|
||||||
|
try:
|
||||||
|
c = self.getconn(tid)
|
||||||
|
c.request(*list(args), **kwargs)
|
||||||
|
return c.getresponse()
|
||||||
|
except:
|
||||||
|
self.closeconn(tid)
|
||||||
|
c = self.getconn(tid)
|
||||||
|
c.request(*list(args), **kwargs)
|
||||||
|
return c.getresponse()
|
||||||
|
|
||||||
|
def listdir(self, path):
|
||||||
|
web_path = self.quotep("/" + "/".join([self.web_root, path])) + "?dots"
|
||||||
|
r = self.sendreq("GET", web_path)
|
||||||
|
if r.status != 200:
|
||||||
|
self.closeconn()
|
||||||
|
raise Exception(
|
||||||
|
"http error {} reading dir {} in {}".format(
|
||||||
|
r.status, web_path, rice_tid()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.parse_html(r)
|
||||||
|
|
||||||
|
def download_file_range(self, path, ofs1, ofs2):
|
||||||
|
web_path = self.quotep("/" + "/".join([self.web_root, path])) + "?raw"
|
||||||
|
hdr_range = "bytes={}-{}".format(ofs1, ofs2 - 1)
|
||||||
|
log("downloading {}".format(hdr_range))
|
||||||
|
|
||||||
|
r = self.sendreq("GET", web_path, headers={"Range": hdr_range})
|
||||||
|
if r.status != http.client.PARTIAL_CONTENT:
|
||||||
|
self.closeconn()
|
||||||
|
raise Exception(
|
||||||
|
"http error {} reading file {} range {} in {}".format(
|
||||||
|
r.status, web_path, hdr_range, rice_tid()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return r.read()
|
||||||
|
|
||||||
|
def parse_html(self, datasrc):
|
||||||
|
ret = []
|
||||||
|
remainder = b""
|
||||||
|
ptn = re.compile(
|
||||||
|
r"^<tr><td>(-|DIR)</td><td><a [^>]+>([^<]+)</a></td><td>([^<]+)</td><td>([^<]+)</td></tr>$"
|
||||||
|
)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
buf = remainder + datasrc.read(4096)
|
||||||
|
# print('[{}]'.format(buf.decode('utf-8')))
|
||||||
|
if not buf:
|
||||||
|
break
|
||||||
|
|
||||||
|
remainder = b""
|
||||||
|
endpos = buf.rfind(b"\n")
|
||||||
|
if endpos >= 0:
|
||||||
|
remainder = buf[endpos + 1 :]
|
||||||
|
buf = buf[:endpos]
|
||||||
|
|
||||||
|
lines = buf.decode("utf-8").split("\n")
|
||||||
|
for line in lines:
|
||||||
|
m = ptn.match(line)
|
||||||
|
if not m:
|
||||||
|
# print(line)
|
||||||
|
continue
|
||||||
|
|
||||||
|
ftype, fname, fsize, fdate = m.groups()
|
||||||
|
fname = html_dec(fname)
|
||||||
|
ts = datetime.strptime(fdate, "%Y-%m-%d %H:%M:%S").timestamp()
|
||||||
|
sz = int(fsize)
|
||||||
|
if ftype == "-":
|
||||||
|
ret.append([fname, self.stat_file(ts, sz), 0])
|
||||||
|
else:
|
||||||
|
ret.append([fname, self.stat_dir(ts, sz), 0])
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def stat_dir(self, ts, sz=4096):
|
||||||
|
ret = Stat()
|
||||||
|
ret.st_mode = stat.S_IFDIR | 0o555
|
||||||
|
ret.st_nlink = 2
|
||||||
|
ret.st_size = sz
|
||||||
|
ret.st_atime = ts
|
||||||
|
ret.st_mtime = ts
|
||||||
|
ret.st_ctime = ts
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def stat_file(self, ts, sz):
|
||||||
|
ret = Stat()
|
||||||
|
ret.st_mode = stat.S_IFREG | 0o444
|
||||||
|
ret.st_size = sz
|
||||||
|
ret.st_atime = ts
|
||||||
|
ret.st_mtime = ts
|
||||||
|
ret.st_ctime = ts
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
class CPPF(Fuse):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
Fuse.__init__(self, *args, **kwargs)
|
||||||
|
|
||||||
|
self.url = None
|
||||||
|
|
||||||
|
self.dircache = []
|
||||||
|
self.dircache_mtx = threading.Lock()
|
||||||
|
|
||||||
|
self.filecache = []
|
||||||
|
self.filecache_mtx = threading.Lock()
|
||||||
|
|
||||||
|
def init2(self):
|
||||||
|
# TODO figure out how python-fuse wanted this to go
|
||||||
|
self.gw = Gateway(self.url) # .decode('utf-8'))
|
||||||
|
info("up")
|
||||||
|
|
||||||
|
def clean_dircache(self):
|
||||||
|
"""not threadsafe"""
|
||||||
|
now = time.time()
|
||||||
|
cutoff = 0
|
||||||
|
for cn in self.dircache:
|
||||||
|
if now - cn.ts > 1:
|
||||||
|
cutoff += 1
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
if cutoff > 0:
|
||||||
|
self.dircache = self.dircache[cutoff:]
|
||||||
|
|
||||||
|
def get_cached_dir(self, dirpath):
|
||||||
|
# with self.dircache_mtx:
|
||||||
|
if True:
|
||||||
|
self.clean_dircache()
|
||||||
|
for cn in self.dircache:
|
||||||
|
if cn.tag == dirpath:
|
||||||
|
return cn
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
"""
|
||||||
|
,-------------------------------, g1>=c1, g2<=c2
|
||||||
|
|cache1 cache2| buf[g1-c1:(g1-c1)+(g2-g1)]
|
||||||
|
`-------------------------------'
|
||||||
|
,---------------,
|
||||||
|
|get1 get2|
|
||||||
|
`---------------'
|
||||||
|
__________________________________________________________________________
|
||||||
|
|
||||||
|
,-------------------------------, g2<=c2, (g2>=c1)
|
||||||
|
|cache1 cache2| cdr=buf[:g2-c1]
|
||||||
|
`-------------------------------' dl car; g1-512K:c1
|
||||||
|
,---------------,
|
||||||
|
|get1 get2|
|
||||||
|
`---------------'
|
||||||
|
__________________________________________________________________________
|
||||||
|
|
||||||
|
,-------------------------------, g1>=c1, (g1<=c2)
|
||||||
|
|cache1 cache2| car=buf[c2-g1:]
|
||||||
|
`-------------------------------' dl cdr; c2:c2+1M
|
||||||
|
,---------------,
|
||||||
|
|get1 get2|
|
||||||
|
`---------------'
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_cached_file(self, path, get1, get2, file_sz):
|
||||||
|
car = None
|
||||||
|
cdr = None
|
||||||
|
ncn = -1
|
||||||
|
# with self.filecache_mtx:
|
||||||
|
if True:
|
||||||
|
dbg("cache request from {} to {}, size {}".format(get1, get2, file_sz))
|
||||||
|
for cn in self.filecache:
|
||||||
|
ncn += 1
|
||||||
|
|
||||||
|
cache_path, cache1 = cn.tag
|
||||||
|
if cache_path != path:
|
||||||
|
continue
|
||||||
|
|
||||||
|
cache2 = cache1 + len(cn.data)
|
||||||
|
if get2 <= cache1 or get1 >= cache2:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if get1 >= cache1 and get2 <= cache2:
|
||||||
|
# keep cache entry alive by moving it to the end
|
||||||
|
self.filecache = (
|
||||||
|
self.filecache[:ncn] + self.filecache[ncn + 1 :] + [cn]
|
||||||
|
)
|
||||||
|
buf_ofs = get1 - cache1
|
||||||
|
buf_end = buf_ofs + (get2 - get1)
|
||||||
|
dbg(
|
||||||
|
"found all ({}, {} to {}, len {}) [{}:{}] = {}".format(
|
||||||
|
ncn,
|
||||||
|
cache1,
|
||||||
|
cache2,
|
||||||
|
len(cn.data),
|
||||||
|
buf_ofs,
|
||||||
|
buf_end,
|
||||||
|
buf_end - buf_ofs,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return cn.data[buf_ofs:buf_end]
|
||||||
|
|
||||||
|
if get2 < cache2:
|
||||||
|
x = cn.data[: get2 - cache1]
|
||||||
|
if not cdr or len(cdr) < len(x):
|
||||||
|
dbg(
|
||||||
|
"found car ({}, {} to {}, len {}) [:{}-{}] = [:{}] = {}".format(
|
||||||
|
ncn,
|
||||||
|
cache1,
|
||||||
|
cache2,
|
||||||
|
len(cn.data),
|
||||||
|
get2,
|
||||||
|
cache1,
|
||||||
|
get2 - cache1,
|
||||||
|
len(x),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
cdr = x
|
||||||
|
|
||||||
|
continue
|
||||||
|
|
||||||
|
if get1 > cache1:
|
||||||
|
x = cn.data[-(cache2 - get1) :]
|
||||||
|
if not car or len(car) < len(x):
|
||||||
|
dbg(
|
||||||
|
"found cdr ({}, {} to {}, len {}) [-({}-{}):] = [-{}:] = {}".format(
|
||||||
|
ncn,
|
||||||
|
cache1,
|
||||||
|
cache2,
|
||||||
|
len(cn.data),
|
||||||
|
cache2,
|
||||||
|
get1,
|
||||||
|
cache2 - get1,
|
||||||
|
len(x),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
car = x
|
||||||
|
|
||||||
|
continue
|
||||||
|
|
||||||
|
raise Exception("what")
|
||||||
|
|
||||||
|
if car and cdr:
|
||||||
|
dbg("<cache> have both")
|
||||||
|
|
||||||
|
ret = car + cdr
|
||||||
|
if len(ret) == get2 - get1:
|
||||||
|
return ret
|
||||||
|
|
||||||
|
raise Exception("{} + {} != {} - {}".format(len(car), len(cdr), get2, get1))
|
||||||
|
|
||||||
|
elif cdr:
|
||||||
|
h_end = get1 + (get2 - get1) - len(cdr)
|
||||||
|
h_ofs = h_end - 512 * 1024
|
||||||
|
|
||||||
|
if h_ofs < 0:
|
||||||
|
h_ofs = 0
|
||||||
|
|
||||||
|
buf_ofs = (get2 - get1) - len(cdr)
|
||||||
|
|
||||||
|
dbg(
|
||||||
|
"<cache> cdr {}, car {}-{}={} [-{}:]".format(
|
||||||
|
len(cdr), h_ofs, h_end, h_end - h_ofs, buf_ofs
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
buf = self.gw.download_file_range(path, h_ofs, h_end)
|
||||||
|
ret = buf[-buf_ofs:] + cdr
|
||||||
|
|
||||||
|
elif car:
|
||||||
|
h_ofs = get1 + len(car)
|
||||||
|
h_end = h_ofs + 1024 * 1024
|
||||||
|
|
||||||
|
if h_end > file_sz:
|
||||||
|
h_end = file_sz
|
||||||
|
|
||||||
|
buf_ofs = (get2 - get1) - len(car)
|
||||||
|
|
||||||
|
dbg(
|
||||||
|
"<cache> car {}, cdr {}-{}={} [:{}]".format(
|
||||||
|
len(car), h_ofs, h_end, h_end - h_ofs, buf_ofs
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
buf = self.gw.download_file_range(path, h_ofs, h_end)
|
||||||
|
ret = car + buf[:buf_ofs]
|
||||||
|
|
||||||
|
else:
|
||||||
|
h_ofs = get1 - 256 * 1024
|
||||||
|
h_end = get2 + 1024 * 1024
|
||||||
|
|
||||||
|
if h_ofs < 0:
|
||||||
|
h_ofs = 0
|
||||||
|
|
||||||
|
if h_end > file_sz:
|
||||||
|
h_end = file_sz
|
||||||
|
|
||||||
|
buf_ofs = get1 - h_ofs
|
||||||
|
buf_end = buf_ofs + get2 - get1
|
||||||
|
|
||||||
|
dbg(
|
||||||
|
"<cache> {}-{}={} [{}:{}]".format(
|
||||||
|
h_ofs, h_end, h_end - h_ofs, buf_ofs, buf_end
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
buf = self.gw.download_file_range(path, h_ofs, h_end)
|
||||||
|
ret = buf[buf_ofs:buf_end]
|
||||||
|
|
||||||
|
cn = CacheNode([path, h_ofs], buf)
|
||||||
|
# with self.filecache_mtx:
|
||||||
|
if True:
|
||||||
|
if len(self.filecache) > 6:
|
||||||
|
self.filecache = self.filecache[1:] + [cn]
|
||||||
|
else:
|
||||||
|
self.filecache.append(cn)
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def _readdir(self, path):
|
||||||
|
path = path.strip("/")
|
||||||
|
log("readdir {}".format(path))
|
||||||
|
|
||||||
|
ret = self.gw.listdir(path)
|
||||||
|
|
||||||
|
# with self.dircache_mtx:
|
||||||
|
if True:
|
||||||
|
cn = CacheNode(path, ret)
|
||||||
|
self.dircache.append(cn)
|
||||||
|
self.clean_dircache()
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def readdir(self, path, offset):
|
||||||
|
for e in self._readdir(path)[offset:]:
|
||||||
|
# log("yield [{}]".format(e[0]))
|
||||||
|
yield fuse.Direntry(e[0])
|
||||||
|
|
||||||
|
def open(self, path, flags):
|
||||||
|
if (flags & (os.O_RDONLY | os.O_WRONLY | os.O_RDWR)) != os.O_RDONLY:
|
||||||
|
return -errno.EACCES
|
||||||
|
|
||||||
|
st = self.getattr(path)
|
||||||
|
try:
|
||||||
|
if st.st_nlink > 0:
|
||||||
|
return st
|
||||||
|
except:
|
||||||
|
return st # -int(os.errcode)
|
||||||
|
|
||||||
|
def read(self, path, length, offset, fh=None, *args):
|
||||||
|
if args:
|
||||||
|
log("unexpected args [" + "] [".join(repr(x) for x in args) + "]")
|
||||||
|
raise Exception()
|
||||||
|
|
||||||
|
path = path.strip("/")
|
||||||
|
|
||||||
|
ofs2 = offset + length
|
||||||
|
log("read {} @ {} len {} end {}".format(path, offset, length, ofs2))
|
||||||
|
|
||||||
|
st = self.getattr(path)
|
||||||
|
try:
|
||||||
|
file_sz = st.st_size
|
||||||
|
except:
|
||||||
|
return st # -int(os.errcode)
|
||||||
|
|
||||||
|
if ofs2 > file_sz:
|
||||||
|
ofs2 = file_sz
|
||||||
|
log("truncate to len {} end {}".format(ofs2 - offset, ofs2))
|
||||||
|
|
||||||
|
if file_sz == 0 or offset >= ofs2:
|
||||||
|
return b""
|
||||||
|
|
||||||
|
# toggle cache here i suppose
|
||||||
|
# return self.get_cached_file(path, offset, ofs2, file_sz)
|
||||||
|
return self.gw.download_file_range(path, offset, ofs2)
|
||||||
|
|
||||||
|
def getattr(self, path):
|
||||||
|
log("getattr [{}]".format(path))
|
||||||
|
|
||||||
|
path = path.strip("/")
|
||||||
|
try:
|
||||||
|
dirpath, fname = path.rsplit("/", 1)
|
||||||
|
except:
|
||||||
|
dirpath = ""
|
||||||
|
fname = path
|
||||||
|
|
||||||
|
if not path:
|
||||||
|
ret = self.gw.stat_dir(time.time())
|
||||||
|
dbg("=root")
|
||||||
|
return ret
|
||||||
|
|
||||||
|
cn = self.get_cached_dir(dirpath)
|
||||||
|
if cn:
|
||||||
|
log("cache ok")
|
||||||
|
dents = cn.data
|
||||||
|
else:
|
||||||
|
log("cache miss")
|
||||||
|
dents = self._readdir(dirpath)
|
||||||
|
|
||||||
|
for cache_name, cache_stat, _ in dents:
|
||||||
|
if cache_name == fname:
|
||||||
|
dbg("=file")
|
||||||
|
return cache_stat
|
||||||
|
|
||||||
|
log("=404")
|
||||||
|
return -errno.ENOENT
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
server = CPPF()
|
||||||
|
server.parser.add_option(mountopt="url", metavar="BASE_URL", default=None)
|
||||||
|
server.parse(values=server, errex=1)
|
||||||
|
if not server.url or not str(server.url).startswith("http"):
|
||||||
|
print("\nerror:")
|
||||||
|
print(" need argument: -o url=<...>")
|
||||||
|
print(" need argument: mount-path")
|
||||||
|
print("example:")
|
||||||
|
print(
|
||||||
|
" ./copyparty-fuseb.py -f -o allow_other,auto_unmount,nonempty,url=http://192.168.1.69:3923 /mnt/nas"
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
server.init2()
|
||||||
|
threading.Thread(target=server.main, daemon=True).start()
|
||||||
|
while True:
|
||||||
|
time.sleep(9001)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -118,7 +118,7 @@ printf ']}' >> /dev/shm/$salt.hs
|
|||||||
|
|
||||||
printf '\033[36m'
|
printf '\033[36m'
|
||||||
|
|
||||||
#curl "http://$target:1234$posturl/handshake.php" -H "Content-Type: text/plain;charset=UTF-8" -H "Cookie: cppwd=$passwd" --data "$(cat "/dev/shm/$salt.hs")" | tee /dev/shm/$salt.res
|
#curl "http://$target:3923$posturl/handshake.php" -H "Content-Type: text/plain;charset=UTF-8" -H "Cookie: cppwd=$passwd" --data "$(cat "/dev/shm/$salt.hs")" | tee /dev/shm/$salt.res
|
||||||
|
|
||||||
{
|
{
|
||||||
{
|
{
|
||||||
@@ -135,7 +135,7 @@ EOF
|
|||||||
cat /dev/shm/$salt.hs
|
cat /dev/shm/$salt.hs
|
||||||
} |
|
} |
|
||||||
tee /dev/shm/$salt.hsb |
|
tee /dev/shm/$salt.hsb |
|
||||||
ncat $target 1234 |
|
ncat $target 3923 |
|
||||||
tee /dev/shm/$salt.hs1r
|
tee /dev/shm/$salt.hs1r
|
||||||
|
|
||||||
wark="$(cat /dev/shm/$salt.hs1r | getwark)"
|
wark="$(cat /dev/shm/$salt.hs1r | getwark)"
|
||||||
@@ -190,7 +190,7 @@ EOF
|
|||||||
nchunk=$((nchunk+1))
|
nchunk=$((nchunk+1))
|
||||||
|
|
||||||
done |
|
done |
|
||||||
ncat $target 1234 |
|
ncat $target 3923 |
|
||||||
tee /dev/shm/$salt.pr
|
tee /dev/shm/$salt.pr
|
||||||
|
|
||||||
t=$(date +%s.%N)
|
t=$(date +%s.%N)
|
||||||
@@ -201,7 +201,7 @@ t=$(date +%s.%N)
|
|||||||
|
|
||||||
printf '\033[36m'
|
printf '\033[36m'
|
||||||
|
|
||||||
ncat $target 1234 < /dev/shm/$salt.hsb |
|
ncat $target 3923 < /dev/shm/$salt.hsb |
|
||||||
tee /dev/shm/$salt.hs2r |
|
tee /dev/shm/$salt.hs2r |
|
||||||
grep -E '"hash": ?\[ *\]'
|
grep -E '"hash": ?\[ *\]'
|
||||||
|
|
||||||
|
|||||||
15
contrib/README.md
Normal file
15
contrib/README.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
### [`copyparty.bat`](copyparty.bat)
|
||||||
|
* launches copyparty with no arguments (anon read+write within same folder)
|
||||||
|
* intended for windows machines with no python.exe in PATH
|
||||||
|
* works on windows, linux and macos
|
||||||
|
* assumes `copyparty-sfx.py` was renamed to `copyparty.py` in the same folder as `copyparty.bat`
|
||||||
|
|
||||||
|
### [`index.html`](index.html)
|
||||||
|
* drop-in redirect from an httpd to copyparty
|
||||||
|
* assumes the webserver and copyparty is running on the same server/IP
|
||||||
|
* modify `10.13.1.1` as necessary if you wish to support browsers without javascript
|
||||||
|
|
||||||
|
# OS integration
|
||||||
|
init-scripts to start copyparty as a service
|
||||||
|
* [`systemd/copyparty.service`](systemd/copyparty.service)
|
||||||
|
* [`openrc/copyparty`](openrc/copyparty)
|
||||||
33
contrib/copyparty.bat
Normal file
33
contrib/copyparty.bat
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
exec python "$(dirname "$0")"/copyparty.py
|
||||||
|
|
||||||
|
@rem on linux, the above will execute and the script will terminate
|
||||||
|
@rem on windows, the rest of this script will run
|
||||||
|
|
||||||
|
@echo off
|
||||||
|
cls
|
||||||
|
|
||||||
|
set py=
|
||||||
|
for /f %%i in ('where python 2^>nul') do (
|
||||||
|
set "py=%%i"
|
||||||
|
goto c1
|
||||||
|
)
|
||||||
|
:c1
|
||||||
|
|
||||||
|
if [%py%] == [] (
|
||||||
|
for /f %%i in ('where /r "%localappdata%\programs\python" python 2^>nul') do (
|
||||||
|
set "py=%%i"
|
||||||
|
goto c2
|
||||||
|
)
|
||||||
|
)
|
||||||
|
:c2
|
||||||
|
|
||||||
|
if [%py%] == [] set "py=c:\python27\python.exe"
|
||||||
|
|
||||||
|
if not exist "%py%" (
|
||||||
|
echo could not find python
|
||||||
|
echo(
|
||||||
|
pause
|
||||||
|
exit /b
|
||||||
|
)
|
||||||
|
|
||||||
|
start cmd /c %py% "%~dp0\copyparty.py"
|
||||||
43
contrib/index.html
Normal file
43
contrib/index.html
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>⇆🎉 redirect</title>
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<style>
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
padding: 1em 2em;
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
font-size: 1.2em;
|
||||||
|
padding: .1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<span id="desc">you probably want</span> <a id="redir" href="//10.13.1.1:3923/">copyparty</a>
|
||||||
|
<script>
|
||||||
|
|
||||||
|
var a = document.getElementById('redir'),
|
||||||
|
proto = window.location.protocol.indexOf('https') === 0 ? 'https' : 'http',
|
||||||
|
loc = window.location.hostname || '127.0.0.1',
|
||||||
|
port = a.getAttribute('href').split(':').pop().split('/')[0],
|
||||||
|
url = proto + '://' + loc + ':' + port + '/';
|
||||||
|
|
||||||
|
a.setAttribute('href', url);
|
||||||
|
document.getElementById('desc').innerHTML = 'redirecting to';
|
||||||
|
|
||||||
|
setTimeout(function() {
|
||||||
|
window.location.href = url;
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
18
contrib/openrc/copyparty
Normal file
18
contrib/openrc/copyparty
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
#!/sbin/openrc-run
|
||||||
|
|
||||||
|
# this will start `/usr/local/bin/copyparty-sfx.py`
|
||||||
|
# and share '/mnt' with anonymous read+write
|
||||||
|
#
|
||||||
|
# installation:
|
||||||
|
# cp -pv copyparty /etc/init.d && rc-update add copyparty
|
||||||
|
#
|
||||||
|
# you may want to:
|
||||||
|
# change '/usr/bin/python' to another interpreter
|
||||||
|
# change '/mnt::a' to another location or permission-set
|
||||||
|
|
||||||
|
name="$SVCNAME"
|
||||||
|
command_background=true
|
||||||
|
pidfile="/var/run/$SVCNAME.pid"
|
||||||
|
|
||||||
|
command="/usr/bin/python /usr/local/bin/copyparty-sfx.py"
|
||||||
|
command_args="-q -v /mnt::a"
|
||||||
19
contrib/systemd/copyparty.service
Normal file
19
contrib/systemd/copyparty.service
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# this will start `/usr/local/bin/copyparty-sfx.py`
|
||||||
|
# and share '/mnt' with anonymous read+write
|
||||||
|
#
|
||||||
|
# installation:
|
||||||
|
# cp -pv copyparty.service /etc/systemd/system && systemctl enable --now copyparty
|
||||||
|
#
|
||||||
|
# you may want to:
|
||||||
|
# change '/usr/bin/python' to another interpreter
|
||||||
|
# change '/mnt::a' to another location or permission-set
|
||||||
|
|
||||||
|
[Unit]
|
||||||
|
Description=copyparty file server
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStart=/usr/bin/python /usr/local/bin/copyparty-sfx.py -q -v /mnt::a
|
||||||
|
ExecStartPre=/bin/bash -c 'mkdir -p /run/tmpfiles.d/ && echo "x /tmp/pe-copyparty*" > /run/tmpfiles.d/copyparty.conf'
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -127,13 +127,18 @@ def main():
|
|||||||
"-c", metavar="PATH", type=str, action="append", help="add config file"
|
"-c", metavar="PATH", type=str, action="append", help="add config file"
|
||||||
)
|
)
|
||||||
ap.add_argument("-i", metavar="IP", type=str, default="0.0.0.0", help="ip to bind")
|
ap.add_argument("-i", metavar="IP", type=str, default="0.0.0.0", help="ip to bind")
|
||||||
ap.add_argument("-p", metavar="PORT", type=int, default=1234, help="port to bind")
|
ap.add_argument("-p", metavar="PORT", type=int, default=3923, help="port to bind")
|
||||||
ap.add_argument("-nc", metavar="NUM", type=int, default=16, help="max num clients")
|
ap.add_argument("-nc", metavar="NUM", type=int, default=64, help="max num clients")
|
||||||
ap.add_argument("-j", metavar="CORES", type=int, help="max num cpu cores")
|
ap.add_argument(
|
||||||
|
"-j", metavar="CORES", type=int, default=1, help="max num cpu cores"
|
||||||
|
)
|
||||||
ap.add_argument("-a", metavar="ACCT", type=str, action="append", help="add account")
|
ap.add_argument("-a", metavar="ACCT", type=str, action="append", help="add account")
|
||||||
ap.add_argument("-v", metavar="VOL", type=str, action="append", help="add volume")
|
ap.add_argument("-v", metavar="VOL", type=str, action="append", help="add volume")
|
||||||
ap.add_argument("-q", action="store_true", help="quiet")
|
ap.add_argument("-q", action="store_true", help="quiet")
|
||||||
ap.add_argument("-nw", action="store_true", help="benchmark: disable writing")
|
ap.add_argument("-ed", action="store_true", help="enable ?dots")
|
||||||
|
ap.add_argument("-nw", action="store_true", help="disable writes (benchmark)")
|
||||||
|
ap.add_argument("-nih", action="store_true", help="no info hostname")
|
||||||
|
ap.add_argument("-nid", action="store_true", help="no info disk-usage")
|
||||||
al = ap.parse_args()
|
al = ap.parse_args()
|
||||||
|
|
||||||
SvcHub(al).run()
|
SvcHub(al).run()
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
|
|
||||||
VERSION = (0, 4, 1)
|
VERSION = (0, 5, 3)
|
||||||
CODENAME = "NIH"
|
CODENAME = "fuse jelly"
|
||||||
BUILD_DT = (2020, 5, 14)
|
BUILD_DT = (2020, 11, 13)
|
||||||
|
|
||||||
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)
|
||||||
|
|||||||
@@ -135,9 +135,9 @@ class AuthSrv(object):
|
|||||||
self.warn_anonwrite = True
|
self.warn_anonwrite = True
|
||||||
|
|
||||||
if WINDOWS:
|
if WINDOWS:
|
||||||
self.re_vol = re.compile(r"^([a-zA-Z]:[\\/][^:]*|[^:]*):([^:]*):(.*)")
|
self.re_vol = re.compile(r"^([a-zA-Z]:[\\/][^:]*|[^:]*):([^:]*):(.*)$")
|
||||||
else:
|
else:
|
||||||
self.re_vol = re.compile(r"^([^:]*):([^:]*):(.*)")
|
self.re_vol = re.compile(r"^([^:]*):([^:]*):(.*)$")
|
||||||
|
|
||||||
self.mutex = threading.Lock()
|
self.mutex = threading.Lock()
|
||||||
self.reload()
|
self.reload()
|
||||||
@@ -220,12 +220,13 @@ class AuthSrv(object):
|
|||||||
if self.args.v:
|
if self.args.v:
|
||||||
# list of src:dst:permset:permset:...
|
# list of src:dst:permset:permset:...
|
||||||
# permset is [rwa]username
|
# permset is [rwa]username
|
||||||
for vol_match in [self.re_vol.match(x) for x in self.args.v]:
|
for v_str in self.args.v:
|
||||||
try:
|
m = self.re_vol.match(v_str)
|
||||||
src, dst, perms = vol_match.groups()
|
if not m:
|
||||||
except:
|
raise Exception("invalid -v argument: [{}]".format(v_str))
|
||||||
raise Exception("invalid -v argument")
|
|
||||||
|
|
||||||
|
src, dst, perms = m.groups()
|
||||||
|
# print("\n".join([src, dst, perms]))
|
||||||
src = fsdec(os.path.abspath(fsenc(src)))
|
src = fsdec(os.path.abspath(fsenc(src)))
|
||||||
dst = dst.strip("/")
|
dst = dst.strip("/")
|
||||||
mount[dst] = src
|
mount[dst] = src
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class BrokerMp(object):
|
|||||||
self.mutex = threading.Lock()
|
self.mutex = threading.Lock()
|
||||||
|
|
||||||
cores = self.args.j
|
cores = self.args.j
|
||||||
if cores is None:
|
if not cores:
|
||||||
cores = mp.cpu_count()
|
cores = mp.cpu_count()
|
||||||
|
|
||||||
self.log("broker", "booting {} subprocesses".format(cores))
|
self.log("broker", "booting {} subprocesses".format(cores))
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import stat
|
|||||||
import gzip
|
import gzip
|
||||||
import time
|
import time
|
||||||
import json
|
import json
|
||||||
|
import socket
|
||||||
|
import ctypes
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import calendar
|
import calendar
|
||||||
|
|
||||||
@@ -14,9 +16,6 @@ from .util import * # noqa # pylint: disable=unused-wildcard-import
|
|||||||
|
|
||||||
if not PY2:
|
if not PY2:
|
||||||
unicode = str
|
unicode = str
|
||||||
from html import escape as html_escape
|
|
||||||
else:
|
|
||||||
from cgi import escape as html_escape # pylint: disable=no-name-in-module
|
|
||||||
|
|
||||||
|
|
||||||
class HttpCli(object):
|
class HttpCli(object):
|
||||||
@@ -25,6 +24,7 @@ class HttpCli(object):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, conn):
|
def __init__(self, conn):
|
||||||
|
self.t0 = time.time()
|
||||||
self.conn = conn
|
self.conn = conn
|
||||||
self.s = conn.s
|
self.s = conn.s
|
||||||
self.sr = conn.sr
|
self.sr = conn.sr
|
||||||
@@ -36,13 +36,13 @@ class HttpCli(object):
|
|||||||
|
|
||||||
self.bufsz = 1024 * 32
|
self.bufsz = 1024 * 32
|
||||||
self.absolute_urls = False
|
self.absolute_urls = False
|
||||||
self.out_headers = {}
|
self.out_headers = {"Access-Control-Allow-Origin": "*"}
|
||||||
|
|
||||||
def log(self, msg):
|
def log(self, msg):
|
||||||
self.log_func(self.log_src, msg)
|
self.log_func(self.log_src, msg)
|
||||||
|
|
||||||
def _check_nonfatal(self, ex):
|
def _check_nonfatal(self, ex):
|
||||||
return ex.code in [403, 404]
|
return ex.code in [404]
|
||||||
|
|
||||||
def _assert_safe_rem(self, rem):
|
def _assert_safe_rem(self, rem):
|
||||||
# sanity check to prevent any disasters
|
# sanity check to prevent any disasters
|
||||||
@@ -87,7 +87,7 @@ class HttpCli(object):
|
|||||||
if "cookie" in self.headers:
|
if "cookie" in self.headers:
|
||||||
cookies = self.headers["cookie"].split(";")
|
cookies = self.headers["cookie"].split(";")
|
||||||
for k, v in [x.split("=", 1) for x in cookies]:
|
for k, v in [x.split("=", 1) for x in cookies]:
|
||||||
if k != "cppwd":
|
if k.strip() != "cppwd":
|
||||||
continue
|
continue
|
||||||
|
|
||||||
v = unescape_cookie(v)
|
v = unescape_cookie(v)
|
||||||
@@ -123,11 +123,20 @@ class HttpCli(object):
|
|||||||
self.uparam = uparam
|
self.uparam = uparam
|
||||||
self.vpath = unquotep(vpath)
|
self.vpath = unquotep(vpath)
|
||||||
|
|
||||||
|
ua = self.headers.get("user-agent", "")
|
||||||
|
if ua.startswith("rclone/"):
|
||||||
|
uparam["raw"] = True
|
||||||
|
uparam["dots"] = True
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if self.mode in ["GET", "HEAD"]:
|
if self.mode in ["GET", "HEAD"]:
|
||||||
return self.handle_get() and self.keepalive
|
return self.handle_get() and self.keepalive
|
||||||
elif self.mode == "POST":
|
elif self.mode == "POST":
|
||||||
return self.handle_post() and self.keepalive
|
return self.handle_post() and self.keepalive
|
||||||
|
elif self.mode == "PUT":
|
||||||
|
return self.handle_put() and self.keepalive
|
||||||
|
elif self.mode == "OPTIONS":
|
||||||
|
return self.handle_options() and self.keepalive
|
||||||
else:
|
else:
|
||||||
raise Pebkac(400, 'invalid HTTP mode "{0}"'.format(self.mode))
|
raise Pebkac(400, 'invalid HTTP mode "{0}"'.format(self.mode))
|
||||||
|
|
||||||
@@ -135,7 +144,7 @@ class HttpCli(object):
|
|||||||
try:
|
try:
|
||||||
# self.log("pebkac at httpcli.run #2: " + repr(ex))
|
# self.log("pebkac at httpcli.run #2: " + repr(ex))
|
||||||
self.keepalive = self._check_nonfatal(ex)
|
self.keepalive = self._check_nonfatal(ex)
|
||||||
self.loud_reply(str(ex), status=ex.code)
|
self.loud_reply("{}: {}".format(str(ex), self.vpath), status=ex.code)
|
||||||
return self.keepalive
|
return self.keepalive
|
||||||
except Pebkac:
|
except Pebkac:
|
||||||
return False
|
return False
|
||||||
@@ -143,9 +152,7 @@ class HttpCli(object):
|
|||||||
def send_headers(self, length, status=200, mime=None, headers={}):
|
def send_headers(self, length, status=200, mime=None, headers={}):
|
||||||
response = ["HTTP/1.1 {} {}".format(status, HTTPCODE[status])]
|
response = ["HTTP/1.1 {} {}".format(status, HTTPCODE[status])]
|
||||||
|
|
||||||
if length is None:
|
if length is not None:
|
||||||
self.keepalive = False
|
|
||||||
else:
|
|
||||||
response.append("Content-Length: " + str(length))
|
response.append("Content-Length: " + str(length))
|
||||||
|
|
||||||
# close if unknown length, otherwise take client's preference
|
# close if unknown length, otherwise take client's preference
|
||||||
@@ -176,7 +183,8 @@ class HttpCli(object):
|
|||||||
self.send_headers(len(body), status, mime, headers)
|
self.send_headers(len(body), status, mime, headers)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.s.sendall(body)
|
if self.mode != "HEAD":
|
||||||
|
self.s.sendall(body)
|
||||||
except:
|
except:
|
||||||
raise Pebkac(400, "client d/c while replying body")
|
raise Pebkac(400, "client d/c while replying body")
|
||||||
|
|
||||||
@@ -184,7 +192,7 @@ class HttpCli(object):
|
|||||||
|
|
||||||
def loud_reply(self, body, *args, **kwargs):
|
def loud_reply(self, body, *args, **kwargs):
|
||||||
self.log(body.rstrip())
|
self.log(body.rstrip())
|
||||||
self.reply(b"<pre>" + body.encode("utf-8"), *list(args), **kwargs)
|
self.reply(b"<pre>" + body.encode("utf-8") + b"\r\n", *list(args), **kwargs)
|
||||||
|
|
||||||
def handle_get(self):
|
def handle_get(self):
|
||||||
logmsg = "{:4} {}".format(self.mode, self.req)
|
logmsg = "{:4} {}".format(self.mode, self.req)
|
||||||
@@ -230,6 +238,30 @@ class HttpCli(object):
|
|||||||
|
|
||||||
return self.tx_browser()
|
return self.tx_browser()
|
||||||
|
|
||||||
|
def handle_options(self):
|
||||||
|
self.log("OPTIONS " + self.req)
|
||||||
|
self.send_headers(
|
||||||
|
None,
|
||||||
|
204,
|
||||||
|
headers={
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
"Access-Control-Allow-Methods": "*",
|
||||||
|
"Access-Control-Allow-Headers": "*",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def handle_put(self):
|
||||||
|
self.log("PUT " + self.req)
|
||||||
|
|
||||||
|
if self.headers.get("expect", "").lower() == "100-continue":
|
||||||
|
try:
|
||||||
|
self.s.sendall(b"HTTP/1.1 100 Continue\r\n\r\n")
|
||||||
|
except:
|
||||||
|
raise Pebkac(400, "client d/c before 100 continue")
|
||||||
|
|
||||||
|
return self.handle_stash()
|
||||||
|
|
||||||
def handle_post(self):
|
def handle_post(self):
|
||||||
self.log("POST " + self.req)
|
self.log("POST " + self.req)
|
||||||
|
|
||||||
@@ -243,6 +275,9 @@ class HttpCli(object):
|
|||||||
if not ctype:
|
if not ctype:
|
||||||
raise Pebkac(400, "you can't post without a content-type header")
|
raise Pebkac(400, "you can't post without a content-type header")
|
||||||
|
|
||||||
|
if "raw" in self.uparam:
|
||||||
|
return self.handle_stash()
|
||||||
|
|
||||||
if "multipart/form-data" in ctype:
|
if "multipart/form-data" in ctype:
|
||||||
return self.handle_post_multipart()
|
return self.handle_post_multipart()
|
||||||
|
|
||||||
@@ -255,6 +290,37 @@ class HttpCli(object):
|
|||||||
|
|
||||||
raise Pebkac(405, "don't know how to handle {} POST".format(ctype))
|
raise Pebkac(405, "don't know how to handle {} POST".format(ctype))
|
||||||
|
|
||||||
|
def handle_stash(self):
|
||||||
|
remains = int(self.headers.get("content-length", None))
|
||||||
|
if remains is None:
|
||||||
|
reader = read_socket_unbounded(self.sr)
|
||||||
|
self.keepalive = False
|
||||||
|
else:
|
||||||
|
reader = read_socket(self.sr, remains)
|
||||||
|
|
||||||
|
vfs, rem = self.conn.auth.vfs.get(self.vpath, self.uname, False, True)
|
||||||
|
fdir = os.path.join(vfs.realpath, rem)
|
||||||
|
|
||||||
|
addr = self.conn.addr[0].replace(":", ".")
|
||||||
|
fn = "put-{:.6f}-{}.bin".format(time.time(), addr)
|
||||||
|
path = os.path.join(fdir, fn)
|
||||||
|
|
||||||
|
with open(path, "wb", 512 * 1024) as f:
|
||||||
|
post_sz, _, sha_b64 = hashcopy(self.conn, reader, f)
|
||||||
|
|
||||||
|
spd = self._spd(post_sz)
|
||||||
|
self.log("{} wrote {}/{} bytes to {}".format(spd, post_sz, remains, path))
|
||||||
|
self.reply("{}\n{}\n".format(post_sz, sha_b64).encode("utf-8"))
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _spd(self, nbytes, add=True):
|
||||||
|
if add:
|
||||||
|
self.conn.nbyte += nbytes
|
||||||
|
|
||||||
|
spd1 = get_spd(nbytes, self.t0)
|
||||||
|
spd2 = get_spd(self.conn.nbyte, self.conn.t0)
|
||||||
|
return spd1 + " " + spd2
|
||||||
|
|
||||||
def handle_post_multipart(self):
|
def handle_post_multipart(self):
|
||||||
self.parser = MultipartParser(self.log, self.sr, self.headers)
|
self.parser = MultipartParser(self.log, self.sr, self.headers)
|
||||||
self.parser.parse()
|
self.parser.parse()
|
||||||
@@ -394,7 +460,9 @@ class HttpCli(object):
|
|||||||
except:
|
except:
|
||||||
self.log("failed to utime ({}, {})".format(path, times))
|
self.log("failed to utime ({}, {})".format(path, times))
|
||||||
|
|
||||||
self.loud_reply("thank")
|
spd = self._spd(post_sz)
|
||||||
|
self.log("{} thank".format(spd))
|
||||||
|
self.reply("thank")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def handle_login(self):
|
def handle_login(self):
|
||||||
@@ -407,7 +475,7 @@ class HttpCli(object):
|
|||||||
msg = "naw dude"
|
msg = "naw dude"
|
||||||
pwd = "x" # nosec
|
pwd = "x" # nosec
|
||||||
|
|
||||||
h = {"Set-Cookie": "cppwd={}; Path=/".format(pwd)}
|
h = {"Set-Cookie": "cppwd={}; Path=/; SameSite=Lax".format(pwd)}
|
||||||
html = self.conn.tpl_msg.render(h1=msg, h2='<a href="/">ack</a>', redir="/")
|
html = self.conn.tpl_msg.render(h1=msg, h2='<a href="/">ack</a>', redir="/")
|
||||||
self.reply(html.encode("utf-8"), headers=h)
|
self.reply(html.encode("utf-8"), headers=h)
|
||||||
return True
|
return True
|
||||||
@@ -440,7 +508,7 @@ class HttpCli(object):
|
|||||||
vpath = "{}/{}".format(self.vpath, sanitized).lstrip("/")
|
vpath = "{}/{}".format(self.vpath, sanitized).lstrip("/")
|
||||||
html = self.conn.tpl_msg.render(
|
html = self.conn.tpl_msg.render(
|
||||||
h2='<a href="/{}">go to /{}</a>'.format(
|
h2='<a href="/{}">go to /{}</a>'.format(
|
||||||
quotep(vpath), html_escape(vpath, quote=False)
|
quotep(vpath), html_escape(vpath)
|
||||||
),
|
),
|
||||||
pre="aight",
|
pre="aight",
|
||||||
click=True,
|
click=True,
|
||||||
@@ -474,7 +542,7 @@ class HttpCli(object):
|
|||||||
vpath = "{}/{}".format(self.vpath, sanitized).lstrip("/")
|
vpath = "{}/{}".format(self.vpath, sanitized).lstrip("/")
|
||||||
html = self.conn.tpl_msg.render(
|
html = self.conn.tpl_msg.render(
|
||||||
h2='<a href="/{}?edit">go to /{}?edit</a>'.format(
|
h2='<a href="/{}?edit">go to /{}?edit</a>'.format(
|
||||||
quotep(vpath), html_escape(vpath, quote=False)
|
quotep(vpath), html_escape(vpath)
|
||||||
),
|
),
|
||||||
pre="aight",
|
pre="aight",
|
||||||
click=True,
|
click=True,
|
||||||
@@ -519,6 +587,7 @@ class HttpCli(object):
|
|||||||
raise Pebkac(400, "empty files in post")
|
raise Pebkac(400, "empty files in post")
|
||||||
|
|
||||||
files.append([sz, sha512_hex])
|
files.append([sz, sha512_hex])
|
||||||
|
self.conn.nbyte += sz
|
||||||
|
|
||||||
except Pebkac:
|
except Pebkac:
|
||||||
if fn != os.devnull:
|
if fn != os.devnull:
|
||||||
@@ -546,7 +615,9 @@ class HttpCli(object):
|
|||||||
# truncated SHA-512 prevents length extension attacks;
|
# truncated SHA-512 prevents length extension attacks;
|
||||||
# using SHA-512/224, optionally SHA-512/256 = :64
|
# using SHA-512/224, optionally SHA-512/256 = :64
|
||||||
|
|
||||||
self.log(msg)
|
vspd = self._spd(sz_total, False)
|
||||||
|
self.log("{} {}".format(vspd, msg))
|
||||||
|
|
||||||
if not nullwrite:
|
if not nullwrite:
|
||||||
# TODO this is bad
|
# TODO this is bad
|
||||||
log_fn = "up.{:.6f}.txt".format(t0)
|
log_fn = "up.{:.6f}.txt".format(t0)
|
||||||
@@ -568,7 +639,7 @@ class HttpCli(object):
|
|||||||
|
|
||||||
html = self.conn.tpl_msg.render(
|
html = self.conn.tpl_msg.render(
|
||||||
h2='<a href="/{}">return to /{}</a>'.format(
|
h2='<a href="/{}">return to /{}</a>'.format(
|
||||||
quotep(self.vpath), html_escape(self.vpath, quote=False)
|
quotep(self.vpath), html_escape(self.vpath)
|
||||||
),
|
),
|
||||||
pre=msg,
|
pre=msg,
|
||||||
)
|
)
|
||||||
@@ -616,7 +687,16 @@ class HttpCli(object):
|
|||||||
|
|
||||||
# if file exists, chekc that timestamp matches the client's
|
# if file exists, chekc that timestamp matches the client's
|
||||||
if srv_lastmod >= 0:
|
if srv_lastmod >= 0:
|
||||||
if cli_lastmod3 not in [-1, srv_lastmod3]:
|
same_lastmod = cli_lastmod3 in [-1, srv_lastmod3]
|
||||||
|
if not same_lastmod:
|
||||||
|
# some filesystems/transports limit precision to 1sec, hopefully floored
|
||||||
|
same_lastmod = (
|
||||||
|
srv_lastmod == int(srv_lastmod)
|
||||||
|
and cli_lastmod3 > srv_lastmod3
|
||||||
|
and cli_lastmod3 - srv_lastmod3 < 1000
|
||||||
|
)
|
||||||
|
|
||||||
|
if not same_lastmod:
|
||||||
response = json.dumps(
|
response = json.dumps(
|
||||||
{
|
{
|
||||||
"ok": False,
|
"ok": False,
|
||||||
@@ -769,11 +849,20 @@ class HttpCli(object):
|
|||||||
else:
|
else:
|
||||||
upper = file_sz
|
upper = file_sz
|
||||||
|
|
||||||
if lower < 0 or lower >= file_sz or upper < 0 or upper > file_sz:
|
if upper > file_sz:
|
||||||
|
upper = file_sz
|
||||||
|
|
||||||
|
if lower < 0 or lower >= upper:
|
||||||
raise Exception()
|
raise Exception()
|
||||||
|
|
||||||
except:
|
except:
|
||||||
raise Pebkac(400, "invalid range requested: " + hrange)
|
err = "invalid range ({}), size={}".format(hrange, file_sz)
|
||||||
|
self.loud_reply(
|
||||||
|
err,
|
||||||
|
status=416,
|
||||||
|
headers={"Content-Range": "bytes */{}".format(file_sz)},
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
status = 206
|
status = 206
|
||||||
self.out_headers["Content-Range"] = "bytes {}-{}/{}".format(
|
self.out_headers["Content-Range"] = "bytes {}-{}/{}".format(
|
||||||
@@ -795,6 +884,9 @@ class HttpCli(object):
|
|||||||
#
|
#
|
||||||
# send reply
|
# send reply
|
||||||
|
|
||||||
|
if not is_compressed:
|
||||||
|
self.out_headers["Cache-Control"] = "no-cache"
|
||||||
|
|
||||||
self.out_headers["Accept-Ranges"] = "bytes"
|
self.out_headers["Accept-Ranges"] = "bytes"
|
||||||
self.send_headers(
|
self.send_headers(
|
||||||
length=upper - lower,
|
length=upper - lower,
|
||||||
@@ -808,6 +900,7 @@ class HttpCli(object):
|
|||||||
self.log(logmsg)
|
self.log(logmsg)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
ret = True
|
||||||
with open_func(*open_args) as f:
|
with open_func(*open_args) as f:
|
||||||
remains = upper - lower
|
remains = upper - lower
|
||||||
f.seek(lower)
|
f.seek(lower)
|
||||||
@@ -820,17 +913,17 @@ class HttpCli(object):
|
|||||||
if remains < len(buf):
|
if remains < len(buf):
|
||||||
buf = buf[:remains]
|
buf = buf[:remains]
|
||||||
|
|
||||||
remains -= len(buf)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.s.sendall(buf)
|
self.s.sendall(buf)
|
||||||
|
remains -= len(buf)
|
||||||
except:
|
except:
|
||||||
logmsg += " \033[31m" + str(upper - remains) + "\033[0m"
|
logmsg += " \033[31m" + str(upper - remains) + "\033[0m"
|
||||||
self.log(logmsg)
|
ret = False
|
||||||
return False
|
break
|
||||||
|
|
||||||
self.log(logmsg)
|
spd = self._spd((upper - lower) - remains)
|
||||||
return True
|
self.log("{}, {}".format(logmsg, spd))
|
||||||
|
return ret
|
||||||
|
|
||||||
def tx_md(self, fs_path):
|
def tx_md(self, fs_path):
|
||||||
logmsg = "{:4} {} ".format("", self.req)
|
logmsg = "{:4} {} ".format("", self.req)
|
||||||
@@ -864,7 +957,7 @@ class HttpCli(object):
|
|||||||
|
|
||||||
targs = {
|
targs = {
|
||||||
"edit": "edit" in self.uparam,
|
"edit": "edit" in self.uparam,
|
||||||
"title": html_escape(self.vpath, quote=False),
|
"title": html_escape(self.vpath),
|
||||||
"lastmod": int(ts_md * 1000),
|
"lastmod": int(ts_md * 1000),
|
||||||
"md": "",
|
"md": "",
|
||||||
}
|
}
|
||||||
@@ -905,7 +998,7 @@ class HttpCli(object):
|
|||||||
else:
|
else:
|
||||||
vpath += "/" + node
|
vpath += "/" + node
|
||||||
|
|
||||||
vpnodes.append([quotep(vpath) + "/", html_escape(node, quote=False)])
|
vpnodes.append([quotep(vpath) + "/", html_escape(node)])
|
||||||
|
|
||||||
vn, rem = self.auth.vfs.get(
|
vn, rem = self.auth.vfs.get(
|
||||||
self.vpath, self.uname, self.readable, self.writable
|
self.vpath, self.uname, self.readable, self.writable
|
||||||
@@ -941,9 +1034,13 @@ class HttpCli(object):
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# show dotfiles if permitted and requested
|
||||||
|
if not self.args.ed or "dots" not in self.uparam:
|
||||||
|
vfs_ls = exclude_dotfiles(vfs_ls)
|
||||||
|
|
||||||
dirs = []
|
dirs = []
|
||||||
files = []
|
files = []
|
||||||
for fn in exclude_dotfiles(vfs_ls):
|
for fn in vfs_ls:
|
||||||
base = ""
|
base = ""
|
||||||
href = fn
|
href = fn
|
||||||
if self.absolute_urls and vpath:
|
if self.absolute_urls and vpath:
|
||||||
@@ -976,7 +1073,7 @@ class HttpCli(object):
|
|||||||
dt = datetime.utcfromtimestamp(inf.st_mtime)
|
dt = datetime.utcfromtimestamp(inf.st_mtime)
|
||||||
dt = dt.strftime("%Y-%m-%d %H:%M:%S")
|
dt = dt.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
item = [margin, quotep(href), html_escape(fn, quote=False), sz, dt]
|
item = [margin, quotep(href), html_escape(fn), sz, dt]
|
||||||
if is_dir:
|
if is_dir:
|
||||||
dirs.append(item)
|
dirs.append(item)
|
||||||
else:
|
else:
|
||||||
@@ -989,6 +1086,45 @@ class HttpCli(object):
|
|||||||
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")
|
||||||
|
|
||||||
|
if False:
|
||||||
|
# this is a mistake
|
||||||
|
md = None
|
||||||
|
for fn in [x[2] for x in files]:
|
||||||
|
if fn.lower() == "readme.md":
|
||||||
|
fn = os.path.join(abspath, fn)
|
||||||
|
with open(fn, "rb") as f:
|
||||||
|
md = f.read().decode("utf-8")
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
|
srv_info = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not self.args.nih:
|
||||||
|
srv_info.append(str(socket.gethostname()).split(".")[0])
|
||||||
|
except:
|
||||||
|
self.log("#wow #whoa")
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
# some fuses misbehave
|
||||||
|
if not self.args.nid:
|
||||||
|
if WINDOWS:
|
||||||
|
bfree = ctypes.c_ulonglong(0)
|
||||||
|
ctypes.windll.kernel32.GetDiskFreeSpaceExW(
|
||||||
|
ctypes.c_wchar_p(abspath), None, None, ctypes.pointer(bfree)
|
||||||
|
)
|
||||||
|
srv_info.append(humansize(bfree.value) + " free")
|
||||||
|
else:
|
||||||
|
sv = os.statvfs(abspath)
|
||||||
|
free = humansize(sv.f_frsize * sv.f_bfree, True)
|
||||||
|
total = humansize(sv.f_frsize * sv.f_blocks, True)
|
||||||
|
|
||||||
|
srv_info.append(free + " free")
|
||||||
|
srv_info.append(total)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
ts = ""
|
ts = ""
|
||||||
# ts = "?{}".format(time.time())
|
# ts = "?{}".format(time.time())
|
||||||
|
|
||||||
@@ -1002,7 +1138,8 @@ class HttpCli(object):
|
|||||||
ts=ts,
|
ts=ts,
|
||||||
prologue=logues[0],
|
prologue=logues[0],
|
||||||
epilogue=logues[1],
|
epilogue=logues[1],
|
||||||
title=html_escape(self.vpath, quote=False),
|
title=html_escape(self.vpath),
|
||||||
|
srv_info="</span> /// <span>".join(srv_info),
|
||||||
)
|
)
|
||||||
self.reply(html.encode("utf-8", "replace"))
|
self.reply(html.encode("utf-8", "replace"))
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from __future__ import print_function, unicode_literals
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import ssl
|
import ssl
|
||||||
|
import time
|
||||||
import socket
|
import socket
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -41,6 +42,8 @@ class HttpConn(object):
|
|||||||
self.auth = hsrv.auth
|
self.auth = hsrv.auth
|
||||||
self.cert_path = hsrv.cert_path
|
self.cert_path = hsrv.cert_path
|
||||||
|
|
||||||
|
self.t0 = time.time()
|
||||||
|
self.nbyte = 0
|
||||||
self.workload = 0
|
self.workload = 0
|
||||||
self.log_func = hsrv.log
|
self.log_func = hsrv.log
|
||||||
self.log_src = "{} \033[36m{}".format(addr[0], addr[1]).ljust(26)
|
self.log_src = "{} \033[36m{}".format(addr[0], addr[1]).ljust(26)
|
||||||
@@ -86,7 +89,7 @@ class HttpConn(object):
|
|||||||
self.s.send(b"HTTP/1.1 400 Bad Request\r\n\r\n" + err.encode("utf-8"))
|
self.s.send(b"HTTP/1.1 400 Bad Request\r\n\r\n" + err.encode("utf-8"))
|
||||||
return
|
return
|
||||||
|
|
||||||
if method not in [None, b"GET ", b"HEAD", b"POST"]:
|
if method not in [None, b"GET ", b"HEAD", b"POST", b"PUT ", b"OPTI"]:
|
||||||
if self.sr:
|
if self.sr:
|
||||||
self.log("\033[1;31mTODO: cannot do https in jython\033[0m")
|
self.log("\033[1;31mTODO: cannot do https in jython\033[0m")
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -80,8 +80,9 @@ class HttpSrv(object):
|
|||||||
"%s %s" % addr,
|
"%s %s" % addr,
|
||||||
"shut_rdwr err:\n {}\n {}".format(repr(sck), ex),
|
"shut_rdwr err:\n {}\n {}".format(repr(sck), ex),
|
||||||
)
|
)
|
||||||
if ex.errno not in [10038, 107, 57, 9]:
|
if ex.errno not in [10038, 10054, 107, 57, 9]:
|
||||||
# 10038 No longer considered a socket
|
# 10038 No longer considered a socket
|
||||||
|
# 10054 Foribly closed by remote
|
||||||
# 107 Transport endpoint not connected
|
# 107 Transport endpoint not connected
|
||||||
# 57 Socket is not connected
|
# 57 Socket is not connected
|
||||||
# 9 Bad file descriptor
|
# 9 Bad file descriptor
|
||||||
|
|||||||
@@ -129,8 +129,8 @@ class SvcHub(object):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def check_mp_enable(self):
|
def check_mp_enable(self):
|
||||||
if self.args.j == 0:
|
if self.args.j == 1:
|
||||||
self.log("root", "multiprocessing disabled by argument -j 0;")
|
self.log("root", "multiprocessing disabled by argument -j 1;")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if mp.cpu_count() <= 1:
|
if mp.cpu_count() <= 1:
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from __future__ import print_function, unicode_literals
|
|||||||
|
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
import base64
|
import base64
|
||||||
import struct
|
import struct
|
||||||
import hashlib
|
import hashlib
|
||||||
@@ -42,6 +43,7 @@ if WINDOWS and PY2:
|
|||||||
|
|
||||||
HTTPCODE = {
|
HTTPCODE = {
|
||||||
200: "OK",
|
200: "OK",
|
||||||
|
204: "No Content",
|
||||||
206: "Partial Content",
|
206: "Partial Content",
|
||||||
304: "Not Modified",
|
304: "Not Modified",
|
||||||
400: "Bad Request",
|
400: "Bad Request",
|
||||||
@@ -49,6 +51,7 @@ HTTPCODE = {
|
|||||||
404: "Not Found",
|
404: "Not Found",
|
||||||
405: "Method Not Allowed",
|
405: "Method Not Allowed",
|
||||||
413: "Payload Too Large",
|
413: "Payload Too Large",
|
||||||
|
416: "Requested Range Not Satisfiable",
|
||||||
422: "Unprocessable Entity",
|
422: "Unprocessable Entity",
|
||||||
500: "Internal Server Error",
|
500: "Internal Server Error",
|
||||||
501: "Not Implemented",
|
501: "Not Implemented",
|
||||||
@@ -309,18 +312,7 @@ def get_boundary(headers):
|
|||||||
def read_header(sr):
|
def read_header(sr):
|
||||||
ret = b""
|
ret = b""
|
||||||
while True:
|
while True:
|
||||||
if ret.endswith(b"\r\n\r\n"):
|
buf = sr.recv(1024)
|
||||||
break
|
|
||||||
elif ret.endswith(b"\r\n\r"):
|
|
||||||
n = 1
|
|
||||||
elif ret.endswith(b"\r\n"):
|
|
||||||
n = 2
|
|
||||||
elif ret.endswith(b"\r"):
|
|
||||||
n = 3
|
|
||||||
else:
|
|
||||||
n = 4
|
|
||||||
|
|
||||||
buf = sr.recv(n)
|
|
||||||
if not buf:
|
if not buf:
|
||||||
if not ret:
|
if not ret:
|
||||||
return None
|
return None
|
||||||
@@ -332,11 +324,40 @@ def read_header(sr):
|
|||||||
)
|
)
|
||||||
|
|
||||||
ret += buf
|
ret += buf
|
||||||
|
ofs = ret.find(b"\r\n\r\n")
|
||||||
|
if ofs < 0:
|
||||||
|
if len(ret) > 1024 * 64:
|
||||||
|
raise Pebkac(400, "header 2big")
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
if len(ret) > 1024 * 64:
|
sr.unrecv(ret[ofs + 4 :])
|
||||||
raise Pebkac(400, "header 2big")
|
return ret[:ofs].decode("utf-8", "surrogateescape").split("\r\n")
|
||||||
|
|
||||||
return ret[:-4].decode("utf-8", "surrogateescape").split("\r\n")
|
|
||||||
|
def humansize(sz, terse=False):
|
||||||
|
for unit in ["B", "KiB", "MiB", "GiB", "TiB"]:
|
||||||
|
if sz < 1024:
|
||||||
|
break
|
||||||
|
|
||||||
|
sz /= 1024.0
|
||||||
|
|
||||||
|
ret = " ".join([str(sz)[:4].rstrip("."), unit])
|
||||||
|
|
||||||
|
if not terse:
|
||||||
|
return ret
|
||||||
|
|
||||||
|
return ret.replace("iB", "").replace(" ", "")
|
||||||
|
|
||||||
|
|
||||||
|
def get_spd(nbyte, t0, t=None):
|
||||||
|
if t is None:
|
||||||
|
t = time.time()
|
||||||
|
|
||||||
|
bps = nbyte / ((t - t0) + 0.001)
|
||||||
|
s1 = humansize(nbyte).replace(" ", "\033[33m").replace("iB", "")
|
||||||
|
s2 = humansize(bps).replace(" ", "\033[35m").replace("iB", "")
|
||||||
|
return "{} \033[0m{}/s\033[0m".format(s1, s2)
|
||||||
|
|
||||||
|
|
||||||
def undot(path):
|
def undot(path):
|
||||||
@@ -388,6 +409,21 @@ def exclude_dotfiles(filepaths):
|
|||||||
yield fpath
|
yield fpath
|
||||||
|
|
||||||
|
|
||||||
|
def html_escape(s, quote=False):
|
||||||
|
"""html.escape but also newlines"""
|
||||||
|
s = (
|
||||||
|
s.replace("&", "&")
|
||||||
|
.replace("<", "<")
|
||||||
|
.replace(">", ">")
|
||||||
|
.replace("\r", " ")
|
||||||
|
.replace("\n", " ")
|
||||||
|
)
|
||||||
|
if quote:
|
||||||
|
s = s.replace('"', """).replace("'", "'")
|
||||||
|
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
def quotep(txt):
|
def quotep(txt):
|
||||||
"""url quoter which deals with bytes correctly"""
|
"""url quoter which deals with bytes correctly"""
|
||||||
btxt = w8enc(txt)
|
btxt = w8enc(txt)
|
||||||
@@ -402,8 +438,8 @@ def quotep(txt):
|
|||||||
def unquotep(txt):
|
def unquotep(txt):
|
||||||
"""url unquoter which deals with bytes correctly"""
|
"""url unquoter which deals with bytes correctly"""
|
||||||
btxt = w8enc(txt)
|
btxt = w8enc(txt)
|
||||||
unq1 = btxt.replace(b"+", b" ")
|
# btxt = btxt.replace(b"+", b" ")
|
||||||
unq2 = unquote(unq1)
|
unq2 = unquote(btxt)
|
||||||
return w8dec(unq2)
|
return w8dec(unq2)
|
||||||
|
|
||||||
|
|
||||||
@@ -451,6 +487,15 @@ def read_socket(sr, total_size):
|
|||||||
yield buf
|
yield buf
|
||||||
|
|
||||||
|
|
||||||
|
def read_socket_unbounded(sr):
|
||||||
|
while True:
|
||||||
|
buf = sr.recv(32 * 1024)
|
||||||
|
if not buf:
|
||||||
|
return
|
||||||
|
|
||||||
|
yield buf
|
||||||
|
|
||||||
|
|
||||||
def hashcopy(actor, fin, fout):
|
def hashcopy(actor, fin, fout):
|
||||||
u32_lim = int((2 ** 31) * 0.9)
|
u32_lim = int((2 ** 31) * 0.9)
|
||||||
hashobj = hashlib.sha512()
|
hashobj = hashlib.sha512()
|
||||||
|
|||||||
@@ -131,6 +131,17 @@ a {
|
|||||||
.logue {
|
.logue {
|
||||||
padding: .2em 1.5em;
|
padding: .2em 1.5em;
|
||||||
}
|
}
|
||||||
|
#srv_info {
|
||||||
|
opacity: .5;
|
||||||
|
font-size: .8em;
|
||||||
|
color: #fc5;
|
||||||
|
position: absolute;
|
||||||
|
top: .5em;
|
||||||
|
left: 2em;
|
||||||
|
}
|
||||||
|
#srv_info span {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
a.play {
|
a.play {
|
||||||
color: #e70;
|
color: #e70;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th></th>
|
<th></th>
|
||||||
<th>File Name</th>
|
<th>File Name</th>
|
||||||
<th>File Size</th>
|
<th sort="int">File Size</th>
|
||||||
<th>Date</th>
|
<th>Date</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -53,6 +53,10 @@
|
|||||||
|
|
||||||
<h2><a href="?h">control-panel</a></h2>
|
<h2><a href="?h">control-panel</a></h2>
|
||||||
|
|
||||||
|
{%- if srv_info %}
|
||||||
|
<div id="srv_info"><span>{{ srv_info }}</span></div>
|
||||||
|
{%- endif %}
|
||||||
|
|
||||||
<div id="widget">
|
<div id="widget">
|
||||||
<div id="wtoggle">♫</div>
|
<div id="wtoggle">♫</div>
|
||||||
<div id="widgeti">
|
<div id="widgeti">
|
||||||
|
|||||||
@@ -106,7 +106,9 @@ function makeSortable(table) {
|
|||||||
if (th) i = th.length;
|
if (th) i = th.length;
|
||||||
else return; // if no `<thead>` then do nothing
|
else return; // if no `<thead>` then do nothing
|
||||||
while (--i >= 0) (function (i) {
|
while (--i >= 0) (function (i) {
|
||||||
th[i].addEventListener('click', function () { sortTable(table, i) });
|
th[i].onclick = function () {
|
||||||
|
sortTable(table, i);
|
||||||
|
};
|
||||||
}(i));
|
}(i));
|
||||||
}
|
}
|
||||||
makeSortable(o('files'));
|
makeSortable(o('files'));
|
||||||
@@ -123,7 +125,6 @@ var mp = (function () {
|
|||||||
'cover_url': ''
|
'cover_url': ''
|
||||||
};
|
};
|
||||||
var re_audio = new RegExp('\.(opus|ogg|m4a|aac|mp3|wav|flac)$', 'i');
|
var re_audio = new RegExp('\.(opus|ogg|m4a|aac|mp3|wav|flac)$', 'i');
|
||||||
var re_cover = new RegExp('^(cover|folder|cd|front|back)\.(jpe?g|png|gif)$', 'i');
|
|
||||||
|
|
||||||
var trs = document.getElementById('files').getElementsByTagName('tbody')[0].getElementsByTagName('tr');
|
var trs = document.getElementById('files').getElementsByTagName('tbody')[0].getElementsByTagName('tr');
|
||||||
for (var a = 0, aa = trs.length; a < aa; a++) {
|
for (var a = 0, aa = trs.length; a < aa; a++) {
|
||||||
@@ -414,15 +415,6 @@ var vbar = (function () {
|
|||||||
var x = e.clientX - rect.left;
|
var x = e.clientX - rect.left;
|
||||||
var mul = x * 1.0 / rect.width;
|
var mul = x * 1.0 / rect.width;
|
||||||
|
|
||||||
/*
|
|
||||||
dbg(//Math.round(rect.width) + 'x' + Math.round(rect.height) + '+' +
|
|
||||||
//Math.round(rect.left) + '+' + Math.round(rect.top) + ', ' +
|
|
||||||
//Math.round(e.clientX) + 'x' + Math.round(e.clientY) + ', ' +
|
|
||||||
Math.round(mp.au.currentTime * 10) / 10 + ', ' +
|
|
||||||
Math.round(mp.au.duration * 10) / 10 + '*' +
|
|
||||||
Math.round(mul * 1000) / 1000);
|
|
||||||
*/
|
|
||||||
|
|
||||||
mp.au.currentTime = mp.au.duration * mul;
|
mp.au.currentTime = mp.au.duration * mul;
|
||||||
|
|
||||||
if (mp.au === mp.au_native)
|
if (mp.au === mp.au_native)
|
||||||
@@ -483,8 +475,14 @@ function setclass(id, clas) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
var iOS = !!navigator.platform &&
|
var need_ogv = true;
|
||||||
/iPad|iPhone|iPod/.test(navigator.platform);
|
try {
|
||||||
|
need_ogv = new Audio().canPlayType('audio/ogg; codecs=opus') !== 'probably';
|
||||||
|
|
||||||
|
if (/ Edge\//.exec(navigator.userAgent + ''))
|
||||||
|
need_ogv = true;
|
||||||
|
}
|
||||||
|
catch (ex) { }
|
||||||
|
|
||||||
|
|
||||||
// plays the tid'th audio file on the page
|
// plays the tid'th audio file on the page
|
||||||
@@ -507,7 +505,7 @@ function play(tid, call_depth) {
|
|||||||
var hack_attempt_play = true;
|
var hack_attempt_play = true;
|
||||||
|
|
||||||
var url = mp.tracks[tid];
|
var url = mp.tracks[tid];
|
||||||
if (iOS && /\.(ogg|opus)$/i.test(url)) {
|
if (need_ogv && /\.(ogg|opus)$/i.test(url)) {
|
||||||
if (mp.au_ogvjs) {
|
if (mp.au_ogvjs) {
|
||||||
mp.au = mp.au_ogvjs;
|
mp.au = mp.au_ogvjs;
|
||||||
}
|
}
|
||||||
@@ -594,7 +592,6 @@ function evau_error(e) {
|
|||||||
err += '\n\nFile: «' + decodeURIComponent(eplaya.src.split('/').slice(-1)[0]) + '»';
|
err += '\n\nFile: «' + decodeURIComponent(eplaya.src.split('/').slice(-1)[0]) + '»';
|
||||||
|
|
||||||
alert(err);
|
alert(err);
|
||||||
play(eplaya.tid + 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -613,7 +610,7 @@ function show_modal(html) {
|
|||||||
function unblocked() {
|
function unblocked() {
|
||||||
var dom = o('blocked');
|
var dom = o('blocked');
|
||||||
if (dom)
|
if (dom)
|
||||||
dom.remove();
|
dom.parentNode.removeChild(dom);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
@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;
|
||||||
@@ -9,6 +13,7 @@ html, body {
|
|||||||
}
|
}
|
||||||
#mw {
|
#mw {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
padding: 0 1.5em;
|
||||||
}
|
}
|
||||||
pre, code, a {
|
pre, code, a {
|
||||||
color: #480;
|
color: #480;
|
||||||
@@ -22,7 +27,7 @@ code {
|
|||||||
font-size: .96em;
|
font-size: .96em;
|
||||||
}
|
}
|
||||||
pre, code {
|
pre, code {
|
||||||
font-family: monospace, monospace;
|
font-family: 'scp', monospace, monospace;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
@@ -42,7 +47,7 @@ pre code {
|
|||||||
pre code:last-child {
|
pre code:last-child {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
pre code:before {
|
pre code::before {
|
||||||
content: counter(precode);
|
content: counter(precode);
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@@ -104,8 +109,12 @@ h2 a, h4 a, h6 a {
|
|||||||
#mp ol>li {
|
#mp ol>li {
|
||||||
margin: .7em 0;
|
margin: .7em 0;
|
||||||
}
|
}
|
||||||
|
strong {
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
p>em,
|
p>em,
|
||||||
li>em {
|
li>em,
|
||||||
|
td>em {
|
||||||
color: #c50;
|
color: #c50;
|
||||||
padding: .1em;
|
padding: .1em;
|
||||||
border-bottom: .1em solid #bbb;
|
border-bottom: .1em solid #bbb;
|
||||||
@@ -168,14 +177,12 @@ small {
|
|||||||
}
|
}
|
||||||
table {
|
table {
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
|
margin: 1em 0;
|
||||||
}
|
}
|
||||||
td {
|
th, td {
|
||||||
padding: .2em .5em;
|
padding: .2em .5em;
|
||||||
border: .12em solid #aaa;
|
border: .12em solid #aaa;
|
||||||
}
|
}
|
||||||
th {
|
|
||||||
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,13 +205,15 @@ blink {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
#mw {
|
#mw {
|
||||||
padding: 0 1em;
|
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
right: 0;
|
right: 0;
|
||||||
}
|
}
|
||||||
#mp {
|
#mp {
|
||||||
max-width: 54em;
|
max-width: 52em;
|
||||||
margin-bottom: 6em;
|
margin-bottom: 6em;
|
||||||
|
word-break: break-word;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
word-wrap: break-word; /*ie*/
|
||||||
}
|
}
|
||||||
a {
|
a {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
@@ -238,12 +247,6 @@ blink {
|
|||||||
z-index: 10;
|
z-index: 10;
|
||||||
width: calc(100% - 1em);
|
width: calc(100% - 1em);
|
||||||
}
|
}
|
||||||
#mn.undocked {
|
|
||||||
position: fixed;
|
|
||||||
padding: 1.7em 0 1.5em 1em;
|
|
||||||
box-shadow: 0 0 .5em rgba(0, 0, 0, 0.3);
|
|
||||||
background: #f7f7f7;
|
|
||||||
}
|
|
||||||
#mn a {
|
#mn a {
|
||||||
color: #444;
|
color: #444;
|
||||||
background: none;
|
background: none;
|
||||||
@@ -261,7 +264,7 @@ blink {
|
|||||||
#mn a:last-child {
|
#mn a:last-child {
|
||||||
padding-right: .5em;
|
padding-right: .5em;
|
||||||
}
|
}
|
||||||
#mn a:not(:last-child):after {
|
#mn a:not(:last-child)::after {
|
||||||
content: '';
|
content: '';
|
||||||
width: 1.05em;
|
width: 1.05em;
|
||||||
height: 1.05em;
|
height: 1.05em;
|
||||||
@@ -290,6 +293,32 @@ blink {
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
#mh a:hover {
|
||||||
|
color: #000;
|
||||||
|
background: #ddd;
|
||||||
|
}
|
||||||
|
#toolsbox {
|
||||||
|
overflow: hidden;
|
||||||
|
display: inline-block;
|
||||||
|
background: #eee;
|
||||||
|
height: 1.5em;
|
||||||
|
padding: 0 .2em;
|
||||||
|
margin: 0 .2em;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
#toolsbox.open {
|
||||||
|
height: auto;
|
||||||
|
overflow: visible;
|
||||||
|
background: #eee;
|
||||||
|
box-shadow: 0 .2em .2em #ccc;
|
||||||
|
padding-bottom: .2em;
|
||||||
|
}
|
||||||
|
#toolsbox a {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
#toolsbox a+a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -333,8 +362,12 @@ blink {
|
|||||||
html.dark #m>ol {
|
html.dark #m>ol {
|
||||||
border-color: #555;
|
border-color: #555;
|
||||||
}
|
}
|
||||||
|
html.dark strong {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
html.dark p>em,
|
html.dark p>em,
|
||||||
html.dark li>em {
|
html.dark li>em,
|
||||||
|
html.dark td>em {
|
||||||
color: #f94;
|
color: #f94;
|
||||||
border-color: #666;
|
border-color: #666;
|
||||||
}
|
}
|
||||||
@@ -355,7 +388,7 @@ blink {
|
|||||||
background: #282828;
|
background: #282828;
|
||||||
border: .07em dashed #444;
|
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);
|
||||||
}
|
}
|
||||||
html.dark #mn a {
|
html.dark #mn a {
|
||||||
@@ -372,21 +405,32 @@ blink {
|
|||||||
color: #ccc;
|
color: #ccc;
|
||||||
background: none;
|
background: none;
|
||||||
}
|
}
|
||||||
|
html.dark #mh a:hover {
|
||||||
|
background: #333;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
html.dark #toolsbox {
|
||||||
|
background: #222;
|
||||||
|
}
|
||||||
|
html.dark #toolsbox.open {
|
||||||
|
box-shadow: 0 .2em .2em #069;
|
||||||
|
border-radius: 0 0 .4em .4em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 70em) {
|
@media screen and (min-width: 66em) {
|
||||||
#mw {
|
#mw {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
left: 14em;
|
left: 14em;
|
||||||
left: calc(100% - 57em);
|
left: calc(100% - 55em);
|
||||||
max-width: none;
|
max-width: none;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
scrollbar-color: #eb0 #f7f7f7;
|
scrollbar-color: #eb0 #f7f7f7;
|
||||||
}
|
}
|
||||||
#toc {
|
#toc {
|
||||||
width: 13em;
|
width: 13em;
|
||||||
width: calc(100% - 57.3em);
|
width: calc(100% - 55.3em);
|
||||||
max-width: 30em;
|
max-width: 30em;
|
||||||
background: #eee;
|
background: #eee;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@@ -433,34 +477,119 @@ blink {
|
|||||||
width: .8em;
|
width: .8em;
|
||||||
}
|
}
|
||||||
html.dark #toc::-webkit-scrollbar-thumb {
|
html.dark #toc::-webkit-scrollbar-thumb {
|
||||||
background: #eb0;
|
background: #b80;
|
||||||
}
|
|
||||||
html.dark #mn.undocked {
|
|
||||||
box-shadow: 0 0 .5em #555;
|
|
||||||
border: none;
|
|
||||||
background: #0a0a0a;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media screen and (min-width: 87.5em) {
|
@media screen and (min-width: 85.5em) {
|
||||||
#toc { width: 30em }
|
#toc { width: 30em }
|
||||||
#mw { left: 30.5em }
|
#mw { left: 30.5em }
|
||||||
}
|
}
|
||||||
@media print {
|
@media print {
|
||||||
|
@page {
|
||||||
|
size: A4;
|
||||||
|
padding: 0;
|
||||||
|
margin: .5in .6in;
|
||||||
|
mso-header-margin: .6in;
|
||||||
|
mso-footer-margin: .6in;
|
||||||
|
mso-paper-source: 0;
|
||||||
|
}
|
||||||
a {
|
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;
|
||||||
}
|
}
|
||||||
#toc {
|
|
||||||
margin: 0 !important;
|
|
||||||
}
|
|
||||||
#toc>ul {
|
#toc>ul {
|
||||||
border-left: .1em solid #84c4dd;
|
border-left: .1em solid #84c4dd;
|
||||||
}
|
}
|
||||||
#mn, #mh {
|
#mn, #mh {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
html, body, #toc, #mw {
|
||||||
|
margin: 0 !important;
|
||||||
|
word-break: break-word;
|
||||||
|
width: 52em;
|
||||||
|
}
|
||||||
|
#toc {
|
||||||
|
margin-left: 1em !important;
|
||||||
|
}
|
||||||
|
#toc a {
|
||||||
|
color: #000 !important;
|
||||||
|
}
|
||||||
|
#toc a::after {
|
||||||
|
/* hopefully supported by browsers eventually */
|
||||||
|
content: leader('.') target-counter(attr(href), page);
|
||||||
|
}
|
||||||
|
a[ctr]::before {
|
||||||
|
content: attr(ctr) '. ';
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
margin: 2em 0;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
margin: 2em 0 0 0;
|
||||||
|
}
|
||||||
|
h1, h2, h3 {
|
||||||
|
page-break-inside: avoid;
|
||||||
|
}
|
||||||
|
h1::after,
|
||||||
|
h2::after,
|
||||||
|
h3::after {
|
||||||
|
content: 'orz';
|
||||||
|
color: transparent;
|
||||||
|
display: block;
|
||||||
|
line-height: 1em;
|
||||||
|
padding: 4em 0 0 0;
|
||||||
|
margin: 0 0 -5em 0;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
page-break-inside: avoid;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
page-break-inside: auto;
|
||||||
|
}
|
||||||
|
tr {
|
||||||
|
page-break-inside: avoid;
|
||||||
|
page-break-after: auto;
|
||||||
|
}
|
||||||
|
thead {
|
||||||
|
display: table-header-group;
|
||||||
|
}
|
||||||
|
tfoot {
|
||||||
|
display: table-footer-group;
|
||||||
|
}
|
||||||
|
#mp a.vis::after {
|
||||||
|
content: ' (' attr(href) ')';
|
||||||
|
border-bottom: 1px solid #bbb;
|
||||||
|
color: #444;
|
||||||
|
}
|
||||||
|
blockquote {
|
||||||
|
border-color: #555;
|
||||||
|
}
|
||||||
|
code {
|
||||||
|
border-color: #bbb;
|
||||||
|
}
|
||||||
|
pre, pre code {
|
||||||
|
border-color: #999;
|
||||||
|
}
|
||||||
|
pre code::before {
|
||||||
|
color: #058;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
html.dark a {
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
html.dark pre,
|
||||||
|
html.dark code {
|
||||||
|
color: #240;
|
||||||
|
}
|
||||||
|
html.dark p>em,
|
||||||
|
html.dark li>em,
|
||||||
|
html.dark td>em {
|
||||||
|
color: #940;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -17,15 +17,23 @@
|
|||||||
<a id="save" href="?edit">save</a>
|
<a id="save" href="?edit">save</a>
|
||||||
<a id="sbs" href="#">sbs</a>
|
<a id="sbs" href="#">sbs</a>
|
||||||
<a id="nsbs" href="#">editor</a>
|
<a id="nsbs" href="#">editor</a>
|
||||||
<a id="help" href="#">help</a>
|
<div id="toolsbox">
|
||||||
|
<a id="tools" href="#">tools</a>
|
||||||
|
<a id="fmt_table" href="#">prettify table (ctrl-k)</a>
|
||||||
|
<a id="iter_uni" href="#">non-ascii: iterate (ctrl-u)</a>
|
||||||
|
<a id="mark_uni" href="#">non-ascii: markup</a>
|
||||||
|
<a id="cfg_uni" href="#">non-ascii: whitelist</a>
|
||||||
|
<a id="help" href="#">help</a>
|
||||||
|
</div>
|
||||||
{%- else %}
|
{%- else %}
|
||||||
<a href="?edit">edit (basic)</a>
|
<a href="?edit">edit (basic)</a>
|
||||||
<a href="?edit2">edit (fancy)</a>
|
<a href="?edit2">edit (fancy)</a>
|
||||||
|
<a href="?raw">view raw</a>
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
</div>
|
</div>
|
||||||
<div id="toc"></div>
|
<div id="toc"></div>
|
||||||
<div id="mtw">
|
<div id="mtw">
|
||||||
<textarea id="mt">{{ md }}</textarea>
|
<textarea id="mt" autocomplete="off">{{ md }}</textarea>
|
||||||
</div>
|
</div>
|
||||||
<div id="mw">
|
<div id="mw">
|
||||||
<div id="ml">
|
<div id="ml">
|
||||||
@@ -39,16 +47,19 @@
|
|||||||
|
|
||||||
{%- if edit %}
|
{%- if edit %}
|
||||||
<div id="helpbox">
|
<div id="helpbox">
|
||||||
<textarea>
|
<textarea autocomplete="off">
|
||||||
|
|
||||||
write markdown (html is permitted)
|
write markdown (most html is 🙆 too)
|
||||||
|
|
||||||
### hotkey list
|
## hotkey list
|
||||||
* `Ctrl-S` to save
|
* `Ctrl-S` to save
|
||||||
|
* `Ctrl-E` to toggle mode
|
||||||
|
* `Ctrl-K` to prettyprint a table
|
||||||
|
* `Ctrl-U` to iterate non-ascii chars
|
||||||
* `Ctrl-H` / `Ctrl-Shift-H` to create a header
|
* `Ctrl-H` / `Ctrl-Shift-H` to create a header
|
||||||
* `TAB` / `Shift-TAB` to indent/dedent a selection
|
* `TAB` / `Shift-TAB` to indent/dedent a selection
|
||||||
|
|
||||||
### toolbar
|
## toolbar
|
||||||
1. toggle dark mode
|
1. toggle dark mode
|
||||||
2. show/hide navigation bar
|
2. show/hide navigation bar
|
||||||
3. save changes on server
|
3. save changes on server
|
||||||
@@ -56,8 +67,56 @@ write markdown (html is permitted)
|
|||||||
5. toggle editor/preview
|
5. toggle editor/preview
|
||||||
6. this thing :^)
|
6. this thing :^)
|
||||||
|
|
||||||
.
|
## markdown
|
||||||
|
|||
|
||||||
|
|--|--|
|
||||||
|
|`**bold**`|**bold**|
|
||||||
|
|`_italic_`|_italic_|
|
||||||
|
|`~~strike~~`|~~strike~~|
|
||||||
|
|`` `code` ``|`code`|
|
||||||
|
|`[](#hotkey-list)`|[](#hotkey-list)|
|
||||||
|
|`[](/foo/bar.md#header)`|[](/foo/bar.md#header)|
|
||||||
|
|`<blink>💯</blink>`|<blink>💯</blink>|
|
||||||
|
|
||||||
|
## tables
|
||||||
|
|left-aligned|centered|right-aligned
|
||||||
|
| ---------- | :----: | ----------:
|
||||||
|
|one |two |three
|
||||||
|
|
||||||
|
|left-aligned|centered|right-aligned
|
||||||
|
| ---------- | :----: | ----------:
|
||||||
|
|one |two |three
|
||||||
|
|
||||||
|
## lists
|
||||||
|
* one
|
||||||
|
* two
|
||||||
|
1. one
|
||||||
|
1. two
|
||||||
|
* one
|
||||||
|
* two
|
||||||
|
1. one
|
||||||
|
1. two
|
||||||
|
|
||||||
|
## headers
|
||||||
|
# level 1
|
||||||
|
## level 2
|
||||||
|
### level 3
|
||||||
|
|
||||||
|
## quote
|
||||||
|
> hello
|
||||||
|
> hello
|
||||||
|
|
||||||
|
## codeblock
|
||||||
|
four spaces (no tab pls)
|
||||||
|
|
||||||
|
## code in lists
|
||||||
|
* foo
|
||||||
|
bar
|
||||||
|
six spaces total
|
||||||
|
* foo
|
||||||
|
bar
|
||||||
|
six spaces total
|
||||||
|
.
|
||||||
</textarea>
|
</textarea>
|
||||||
</div>
|
</div>
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
|||||||
@@ -30,6 +30,24 @@ function cls(dom, name, add) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function static(obj) {
|
||||||
|
return JSON.parse(JSON.stringify(obj));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// dodge browser issues
|
||||||
|
(function () {
|
||||||
|
var ua = navigator.userAgent;
|
||||||
|
if (ua.indexOf(') Gecko/') !== -1 && /Linux| Mac /.exec(ua)) {
|
||||||
|
// necessary on ff-68.7 at least
|
||||||
|
var s = document.createElement('style');
|
||||||
|
s.innerHTML = '@page { margin: .5in .6in .8in .6in; }';
|
||||||
|
console.log(s.innerHTML);
|
||||||
|
document.head.appendChild(s);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
|
||||||
// add navbar
|
// add navbar
|
||||||
(function () {
|
(function () {
|
||||||
var n = document.location + '';
|
var n = document.location + '';
|
||||||
@@ -136,7 +154,7 @@ function copydom(src, dst, lv) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function convert_markdown(md_text) {
|
function convert_markdown(md_text, dest_dom) {
|
||||||
marked.setOptions({
|
marked.setOptions({
|
||||||
//headerPrefix: 'h-',
|
//headerPrefix: 'h-',
|
||||||
breaks: true,
|
breaks: true,
|
||||||
@@ -145,8 +163,19 @@ function convert_markdown(md_text) {
|
|||||||
var md_html = marked(md_text);
|
var md_html = marked(md_text);
|
||||||
var md_dom = new DOMParser().parseFromString(md_html, "text/html").body;
|
var md_dom = new DOMParser().parseFromString(md_html, "text/html").body;
|
||||||
|
|
||||||
|
var nodes = md_dom.getElementsByTagName('a');
|
||||||
|
for (var a = nodes.length - 1; a >= 0; a--) {
|
||||||
|
var href = nodes[a].getAttribute('href');
|
||||||
|
var txt = nodes[a].textContent;
|
||||||
|
|
||||||
|
if (!txt)
|
||||||
|
nodes[a].textContent = href;
|
||||||
|
else if (href !== txt)
|
||||||
|
nodes[a].setAttribute('class', 'vis');
|
||||||
|
}
|
||||||
|
|
||||||
// todo-lists (should probably be a marked extension)
|
// todo-lists (should probably be a marked extension)
|
||||||
var nodes = md_dom.getElementsByTagName('input');
|
nodes = md_dom.getElementsByTagName('input');
|
||||||
for (var a = nodes.length - 1; a >= 0; a--) {
|
for (var a = nodes.length - 1; a >= 0; a--) {
|
||||||
var dom_box = nodes[a];
|
var dom_box = nodes[a];
|
||||||
if (dom_box.getAttribute('type') !== 'checkbox')
|
if (dom_box.getAttribute('type') !== 'checkbox')
|
||||||
@@ -213,7 +242,7 @@ function convert_markdown(md_text) {
|
|||||||
el.innerHTML = '<a href="#' + id + '">' + el.innerHTML + '</a>';
|
el.innerHTML = '<a href="#' + id + '">' + el.innerHTML + '</a>';
|
||||||
}
|
}
|
||||||
|
|
||||||
copydom(md_dom, dom_pre, 0);
|
copydom(md_dom, dest_dom, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -225,6 +254,7 @@ function init_toc() {
|
|||||||
var anchor = null; // current toc node
|
var anchor = null; // current toc node
|
||||||
var html = []; // generated toc html
|
var html = []; // generated toc html
|
||||||
var lv = 0; // current indentation level in the toc html
|
var lv = 0; // current indentation level in the toc html
|
||||||
|
var ctr = [0, 0, 0, 0, 0, 0];
|
||||||
|
|
||||||
var manip_nodes_dyn = dom_pre.getElementsByTagName('*');
|
var manip_nodes_dyn = dom_pre.getElementsByTagName('*');
|
||||||
var manip_nodes = [];
|
var manip_nodes = [];
|
||||||
@@ -245,6 +275,11 @@ function init_toc() {
|
|||||||
html.push('</ul>');
|
html.push('</ul>');
|
||||||
lv--;
|
lv--;
|
||||||
}
|
}
|
||||||
|
ctr[lv - 1]++;
|
||||||
|
for (var b = lv; b < 6; b++)
|
||||||
|
ctr[b] = 0;
|
||||||
|
|
||||||
|
elm.childNodes[0].setAttribute('ctr', ctr.slice(0, lv).join('.'));
|
||||||
|
|
||||||
html.push('<li>' + elm.innerHTML + '</li>');
|
html.push('<li>' + elm.innerHTML + '</li>');
|
||||||
|
|
||||||
@@ -328,7 +363,7 @@ function init_toc() {
|
|||||||
|
|
||||||
|
|
||||||
// "main" :p
|
// "main" :p
|
||||||
convert_markdown(dom_src.value);
|
convert_markdown(dom_src.value, dom_pre);
|
||||||
var toc = init_toc();
|
var toc = init_toc();
|
||||||
|
|
||||||
|
|
||||||
@@ -360,40 +395,10 @@ var redraw = (function () {
|
|||||||
|
|
||||||
|
|
||||||
dom_navtgl.onclick = function () {
|
dom_navtgl.onclick = function () {
|
||||||
var timeout = null;
|
|
||||||
function show_nav(e) {
|
|
||||||
if (e && e.target == dom_hbar && e.pageX && e.pageX < dom_hbar.offsetWidth / 2)
|
|
||||||
return;
|
|
||||||
|
|
||||||
clearTimeout(timeout);
|
|
||||||
dom_nav.style.display = 'block';
|
|
||||||
}
|
|
||||||
function hide_nav() {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
timeout = setTimeout(function () {
|
|
||||||
dom_nav.style.display = 'none';
|
|
||||||
}, 30);
|
|
||||||
}
|
|
||||||
var hidden = dom_navtgl.innerHTML == 'hide nav';
|
var hidden = dom_navtgl.innerHTML == 'hide nav';
|
||||||
dom_navtgl.innerHTML = hidden ? 'show nav' : 'hide nav';
|
dom_navtgl.innerHTML = hidden ? 'show nav' : 'hide nav';
|
||||||
if (hidden) {
|
dom_nav.style.display = hidden ? 'none' : 'block';
|
||||||
dom_nav.setAttribute('class', 'undocked');
|
|
||||||
dom_nav.style.display = 'none';
|
|
||||||
dom_nav.style.top = dom_hbar.offsetHeight + 'px';
|
|
||||||
dom_nav.onmouseenter = show_nav;
|
|
||||||
dom_nav.onmouseleave = hide_nav;
|
|
||||||
dom_hbar.onmouseenter = show_nav;
|
|
||||||
dom_hbar.onmouseleave = hide_nav;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
dom_nav.setAttribute('class', '');
|
|
||||||
dom_nav.style.display = 'block';
|
|
||||||
dom_nav.style.top = '0';
|
|
||||||
dom_nav.onmouseenter = null;
|
|
||||||
dom_nav.onmouseleave = null;
|
|
||||||
dom_hbar.onmouseenter = null;
|
|
||||||
dom_hbar.onmouseleave = null;
|
|
||||||
}
|
|
||||||
if (window.localStorage)
|
if (window.localStorage)
|
||||||
localStorage.setItem('hidenav', hidden ? 1 : 0);
|
localStorage.setItem('hidenav', hidden ? 1 : 0);
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,12 @@
|
|||||||
#mtw {
|
#mtw {
|
||||||
display: block;
|
display: block;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
left: 0;
|
left: .5em;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
width: calc(100% - 58em);
|
width: calc(100% - 56em);
|
||||||
}
|
}
|
||||||
#mw {
|
#mw {
|
||||||
left: calc(100% - 57em);
|
left: calc(100% - 55em);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
@@ -30,11 +30,11 @@
|
|||||||
#mw.single {
|
#mw.single {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
left: 1em;
|
left: 1em;
|
||||||
left: max(1em, calc((100% - 58em) / 2));
|
left: max(1em, calc((100% - 56em) / 2));
|
||||||
}
|
}
|
||||||
#mtw.single {
|
#mtw.single {
|
||||||
width: 57em;
|
width: 55em;
|
||||||
width: min(57em, calc(100% - 2em));
|
width: min(55em, calc(100% - 2em));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
}
|
}
|
||||||
#mt, #mtr {
|
#mt, #mtr {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: calc(100% - 5px);
|
height: calc(100% - 1px);
|
||||||
color: #444;
|
color: #444;
|
||||||
background: #f7f7f7;
|
background: #f7f7f7;
|
||||||
border: 1px solid #999;
|
border: 1px solid #999;
|
||||||
|
|||||||
@@ -2,10 +2,16 @@
|
|||||||
var server_md = dom_src.value;
|
var server_md = dom_src.value;
|
||||||
|
|
||||||
|
|
||||||
|
// the non-ascii whitelist
|
||||||
|
var esc_uni_whitelist = '\\n\\t\\x20-\\x7eÆØÅæøå';
|
||||||
|
var js_uni_whitelist = eval('\'' + esc_uni_whitelist + '\'');
|
||||||
|
|
||||||
|
|
||||||
// dom nodes
|
// dom nodes
|
||||||
var dom_swrap = document.getElementById('mtw');
|
var dom_swrap = document.getElementById('mtw');
|
||||||
var dom_sbs = document.getElementById('sbs');
|
var dom_sbs = document.getElementById('sbs');
|
||||||
var dom_nsbs = document.getElementById('nsbs');
|
var dom_nsbs = document.getElementById('nsbs');
|
||||||
|
var dom_tbox = document.getElementById('toolsbox');
|
||||||
var dom_ref = (function () {
|
var dom_ref = (function () {
|
||||||
var d = document.createElement('div');
|
var d = document.createElement('div');
|
||||||
d.setAttribute('id', 'mtr');
|
d.setAttribute('id', 'mtr');
|
||||||
@@ -19,14 +25,12 @@ var dom_ref = (function () {
|
|||||||
|
|
||||||
|
|
||||||
// line->scrollpos maps
|
// line->scrollpos maps
|
||||||
var map_src = [];
|
function genmapq(dom, query) {
|
||||||
var map_pre = [];
|
|
||||||
function genmap(dom) {
|
|
||||||
var ret = [];
|
var ret = [];
|
||||||
var last_y = -1;
|
var last_y = -1;
|
||||||
var parent_y = 0;
|
var parent_y = 0;
|
||||||
var parent_n = null;
|
var parent_n = null;
|
||||||
var nodes = dom.querySelectorAll('*[data-ln]');
|
var nodes = dom.querySelectorAll(query);
|
||||||
for (var a = 0; a < nodes.length; a++) {
|
for (var a = 0; a < nodes.length; a++) {
|
||||||
var n = nodes[a];
|
var n = nodes[a];
|
||||||
var ln = parseInt(n.getAttribute('data-ln'));
|
var ln = parseInt(n.getAttribute('data-ln'));
|
||||||
@@ -35,7 +39,7 @@ function genmap(dom) {
|
|||||||
|
|
||||||
var y = 0;
|
var y = 0;
|
||||||
var par = n.offsetParent;
|
var par = n.offsetParent;
|
||||||
if (par != parent_n) {
|
if (par && par != parent_n) {
|
||||||
while (par && par != dom) {
|
while (par && par != dom) {
|
||||||
y += par.offsetTop;
|
y += par.offsetTop;
|
||||||
par = par.offsetParent;
|
par = par.offsetParent;
|
||||||
@@ -49,7 +53,7 @@ function genmap(dom) {
|
|||||||
while (ln > ret.length)
|
while (ln > ret.length)
|
||||||
ret.push(null);
|
ret.push(null);
|
||||||
|
|
||||||
var y = parent_y + n.offsetTop;
|
y = parent_y + n.offsetTop;
|
||||||
if (y <= last_y)
|
if (y <= last_y)
|
||||||
//console.log('awawa');
|
//console.log('awawa');
|
||||||
continue;
|
continue;
|
||||||
@@ -60,6 +64,25 @@ function genmap(dom) {
|
|||||||
}
|
}
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
var map_src = [];
|
||||||
|
var map_pre = [];
|
||||||
|
function genmap(dom, oldmap) {
|
||||||
|
var find = nlines;
|
||||||
|
while (oldmap && find --> 0) {
|
||||||
|
var tmap = genmapq(dom, '*[data-ln="' + find + '"]');
|
||||||
|
if (!tmap || !tmap.length)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var cy = tmap[find];
|
||||||
|
var oy = parseInt(oldmap[find]);
|
||||||
|
if (cy + 24 > oy && cy - 24 < oy)
|
||||||
|
return oldmap;
|
||||||
|
|
||||||
|
console.log('map regen', dom.getAttribute('id'), find, oy, cy, oy - cy);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return genmapq(dom, '*[data-ln]');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// input handler
|
// input handler
|
||||||
@@ -70,7 +93,7 @@ var draw_md = (function () {
|
|||||||
function draw_md() {
|
function draw_md() {
|
||||||
var t0 = new Date().getTime();
|
var t0 = new Date().getTime();
|
||||||
var src = dom_src.value;
|
var src = dom_src.value;
|
||||||
convert_markdown(src);
|
convert_markdown(src, dom_pre);
|
||||||
|
|
||||||
var lines = hesc(src).replace(/\r/g, "").split('\n');
|
var lines = hesc(src).replace(/\r/g, "").split('\n');
|
||||||
nlines = lines.length;
|
nlines = lines.length;
|
||||||
@@ -79,13 +102,13 @@ var draw_md = (function () {
|
|||||||
html.push('<span data-ln="' + (a + 1) + '">' + lines[a] + "</span>");
|
html.push('<span data-ln="' + (a + 1) + '">' + lines[a] + "</span>");
|
||||||
|
|
||||||
dom_ref.innerHTML = html.join('\n');
|
dom_ref.innerHTML = html.join('\n');
|
||||||
map_src = genmap(dom_ref);
|
map_src = genmap(dom_ref, map_src);
|
||||||
map_pre = genmap(dom_pre);
|
map_pre = genmap(dom_pre, map_pre);
|
||||||
|
|
||||||
cls(document.getElementById('save'), 'disabled', src == server_md);
|
cls(document.getElementById('save'), 'disabled', src == server_md);
|
||||||
|
|
||||||
var t1 = new Date().getTime();
|
var t1 = new Date().getTime();
|
||||||
delay = t1 - t0 > 150 ? 25 : 1;
|
delay = t1 - t0 > 100 ? 25 : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
var timeout = null;
|
var timeout = null;
|
||||||
@@ -108,8 +131,8 @@ redraw = (function () {
|
|||||||
dom_wrap.style.top = y;
|
dom_wrap.style.top = y;
|
||||||
dom_swrap.style.top = y;
|
dom_swrap.style.top = y;
|
||||||
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 = genmap(dom_ref, map_src);
|
||||||
map_pre = genmap(dom_pre);
|
map_pre = genmap(dom_pre, map_pre);
|
||||||
dbg(document.body.clientWidth + 'x' + document.body.clientHeight);
|
dbg(document.body.clientWidth + 'x' + document.body.clientHeight);
|
||||||
}
|
}
|
||||||
function setsbs() {
|
function setsbs() {
|
||||||
@@ -147,7 +170,7 @@ redraw = (function () {
|
|||||||
dst.scrollTop = 0;
|
dst.scrollTop = 0;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (y + 8 + src.clientHeight > src.scrollHeight) {
|
if (y + 48 + src.clientHeight > src.scrollHeight) {
|
||||||
dst.scrollTop = dst.scrollHeight - dst.clientHeight;
|
dst.scrollTop = dst.scrollHeight - dst.clientHeight;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -204,7 +227,7 @@ function save(e) {
|
|||||||
save_cls = save_btn.getAttribute('class') + '';
|
save_cls = save_btn.getAttribute('class') + '';
|
||||||
|
|
||||||
if (save_cls.indexOf('disabled') >= 0) {
|
if (save_cls.indexOf('disabled') >= 0) {
|
||||||
alert('there is nothing to save');
|
toast('font-size:2em;color:#fc6;width:9em;', 'no changes');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,7 +244,7 @@ function save(e) {
|
|||||||
fd.append("lastmod", (force ? -1 : last_modified));
|
fd.append("lastmod", (force ? -1 : last_modified));
|
||||||
fd.append("body", txt);
|
fd.append("body", txt);
|
||||||
|
|
||||||
var url = (document.location + '').split('?')[0] + '?raw';
|
var url = (document.location + '').split('?')[0];
|
||||||
var xhr = new XMLHttpRequest();
|
var xhr = new XMLHttpRequest();
|
||||||
xhr.open('POST', url, true);
|
xhr.open('POST', url, true);
|
||||||
xhr.responseType = 'text';
|
xhr.responseType = 'text';
|
||||||
@@ -272,19 +295,24 @@ function save_cb() {
|
|||||||
this.btn.classList.remove('force-save');
|
this.btn.classList.remove('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);
|
||||||
|
}
|
||||||
|
|
||||||
|
function run_savechk(lastmod, txt, btn, ntry) {
|
||||||
// download the saved doc from the server and compare
|
// download the saved doc from the server and compare
|
||||||
var url = (document.location + '').split('?')[0] + '?raw';
|
var url = (document.location + '').split('?')[0] + '?raw&_=' + new Date().getTime();
|
||||||
var xhr = new XMLHttpRequest();
|
var xhr = new XMLHttpRequest();
|
||||||
xhr.open('GET', url, true);
|
xhr.open('GET', url, true);
|
||||||
xhr.responseType = 'text';
|
xhr.responseType = 'text';
|
||||||
xhr.onreadystatechange = save_chk;
|
xhr.onreadystatechange = savechk_cb;
|
||||||
xhr.btn = this.save_btn;
|
xhr.lastmod = lastmod;
|
||||||
xhr.txt = this.txt;
|
xhr.txt = txt;
|
||||||
xhr.lastmod = r.lastmod;
|
xhr.btn = btn;
|
||||||
|
xhr.ntry = ntry;
|
||||||
xhr.send();
|
xhr.send();
|
||||||
}
|
}
|
||||||
|
|
||||||
function save_chk() {
|
function savechk_cb() {
|
||||||
if (this.readyState != XMLHttpRequest.DONE)
|
if (this.readyState != XMLHttpRequest.DONE)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -296,6 +324,14 @@ function save_chk() {
|
|||||||
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) {
|
||||||
|
var that = this;
|
||||||
|
if (that.ntry < 10) {
|
||||||
|
// qnap funny, try a few more times
|
||||||
|
setTimeout(function () {
|
||||||
|
run_savechk(that.lastmod, that.txt, that.btn, that.ntry + 1)
|
||||||
|
}, 100);
|
||||||
|
return;
|
||||||
|
}
|
||||||
alert(
|
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
|
||||||
@@ -308,10 +344,15 @@ function save_chk() {
|
|||||||
last_modified = this.lastmod;
|
last_modified = this.lastmod;
|
||||||
server_md = this.txt;
|
server_md = this.txt;
|
||||||
draw_md();
|
draw_md();
|
||||||
|
toast('font-size:6em;font-family:serif;color:#cf6;width:4em;',
|
||||||
|
'OK✔️<span style="font-size:.2em;color:#999;position:absolute">' + this.ntry + '</span>');
|
||||||
|
}
|
||||||
|
|
||||||
|
function toast(style, msg) {
|
||||||
var ok = document.createElement('div');
|
var ok = document.createElement('div');
|
||||||
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');
|
style += 'font-weight:bold;background:#444;border-radius:.3em;padding:.6em 0;position:fixed;top:30%;left:calc(50% - 2em);text-align:center;z-index:9001;transition:opacity 0.2s ease-in-out;opacity:1';
|
||||||
ok.innerHTML = 'OK✔️';
|
ok.setAttribute('style', style);
|
||||||
|
ok.innerHTML = msg;
|
||||||
var parent = document.getElementById('m');
|
var parent = document.getElementById('m');
|
||||||
document.documentElement.appendChild(ok);
|
document.documentElement.appendChild(ok);
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
@@ -323,13 +364,26 @@ function save_chk() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// firefox bug: initial selection offset isn't cleared properly through js
|
||||||
|
var ff_clearsel = (function () {
|
||||||
|
if (navigator.userAgent.indexOf(') Gecko/') === -1)
|
||||||
|
return function () { }
|
||||||
|
|
||||||
|
return function () {
|
||||||
|
var txt = dom_src.value;
|
||||||
|
var y = dom_src.scrollTop;
|
||||||
|
dom_src.value = '';
|
||||||
|
dom_src.value = txt;
|
||||||
|
dom_src.scrollTop = y;
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
|
||||||
// returns car/cdr (selection bounds) and n1/n2 (grown to full lines)
|
// returns car/cdr (selection bounds) and n1/n2 (grown to full lines)
|
||||||
function linebounds(just_car) {
|
function linebounds(just_car, greedy_growth) {
|
||||||
var car = dom_src.selectionStart,
|
var car = dom_src.selectionStart,
|
||||||
cdr = dom_src.selectionEnd;
|
cdr = dom_src.selectionEnd;
|
||||||
|
|
||||||
dbg(car, cdr);
|
|
||||||
|
|
||||||
if (just_car)
|
if (just_car)
|
||||||
cdr = car;
|
cdr = car;
|
||||||
|
|
||||||
@@ -337,11 +391,13 @@ function linebounds(just_car) {
|
|||||||
n1 = Math.max(car, 0),
|
n1 = Math.max(car, 0),
|
||||||
n2 = Math.min(cdr, md.length - 1);
|
n2 = Math.min(cdr, md.length - 1);
|
||||||
|
|
||||||
if (n1 < n2 && md[n1] == '\n')
|
if (greedy_growth !== true) {
|
||||||
n1++;
|
if (n1 < n2 && md[n1] == '\n')
|
||||||
|
n1++;
|
||||||
|
|
||||||
if (n1 < n2 && md[n2 - 1] == '\n')
|
if (n1 < n2 && md[n2 - 1] == '\n')
|
||||||
n2 -= 2;
|
n2 -= 2;
|
||||||
|
}
|
||||||
|
|
||||||
n1 = md.lastIndexOf('\n', n1 - 1) + 1;
|
n1 = md.lastIndexOf('\n', n1 - 1) + 1;
|
||||||
n2 = md.indexOf('\n', n2);
|
n2 = md.indexOf('\n', n2);
|
||||||
@@ -375,8 +431,11 @@ function setsel(s) {
|
|||||||
s.cdr = s.pre.length + s.sel.length;
|
s.cdr = s.pre.length + s.sel.length;
|
||||||
}
|
}
|
||||||
dom_src.value = [s.pre, s.sel, s.post].join('');
|
dom_src.value = [s.pre, s.sel, s.post].join('');
|
||||||
dom_src.setSelectionRange(s.car, s.cdr);
|
dom_src.setSelectionRange(s.car, s.cdr, dom_src.selectionDirection);
|
||||||
dom_src.oninput();
|
dom_src.oninput();
|
||||||
|
// support chrome:
|
||||||
|
dom_src.blur();
|
||||||
|
dom_src.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -416,17 +475,32 @@ function md_header(dedent) {
|
|||||||
|
|
||||||
// smart-home
|
// smart-home
|
||||||
function md_home(shift) {
|
function md_home(shift) {
|
||||||
var s = linebounds(!shift),
|
var s = linebounds(false, true),
|
||||||
ln = s.md.substring(s.n1, s.n2),
|
ln = s.md.substring(s.n1, s.n2),
|
||||||
m = /^[ \t#>+-]*(\* )?([0-9]+\. +)?/.exec(ln),
|
dir = dom_src.selectionDirection,
|
||||||
home = s.n1 + m[0].length,
|
rev = dir === 'backward',
|
||||||
car = (s.car == home) ? s.n1 : home,
|
p1 = rev ? s.car : s.cdr,
|
||||||
cdr = shift ? s.cdr : car;
|
p2 = rev ? s.cdr : s.car,
|
||||||
|
home = 0,
|
||||||
|
lf = ln.lastIndexOf('\n') + 1,
|
||||||
|
re = /^[ \t#>+-]*(\* )?([0-9]+\. +)?/;
|
||||||
|
|
||||||
if (car > cdr)
|
if (rev)
|
||||||
car = [cdr, cdr = car][0];
|
home = s.n1 + re.exec(ln)[0].length;
|
||||||
|
else
|
||||||
|
home = s.n1 + lf + re.exec(ln.substring(lf))[0].length;
|
||||||
|
|
||||||
dom_src.setSelectionRange(car, cdr);
|
p1 = (p1 !== home) ? home : (rev ? s.n1 : s.n1 + lf);
|
||||||
|
if (!shift)
|
||||||
|
p2 = p1;
|
||||||
|
|
||||||
|
if (rev !== p1 < p2)
|
||||||
|
dir = rev ? 'forward' : 'backward';
|
||||||
|
|
||||||
|
if (!shift)
|
||||||
|
ff_clearsel();
|
||||||
|
|
||||||
|
dom_src.setSelectionRange(Math.min(p1, p2), Math.max(p1, p2), dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -434,13 +508,269 @@ function md_home(shift) {
|
|||||||
function md_newline() {
|
function md_newline() {
|
||||||
var s = linebounds(true),
|
var s = linebounds(true),
|
||||||
ln = s.md.substring(s.n1, s.n2),
|
ln = s.md.substring(s.n1, s.n2),
|
||||||
m = /^[ \t>+-]*(\* )?([0-9]+\. +)?/.exec(ln);
|
m1 = /^( *)([0-9]+)(\. +)/.exec(ln),
|
||||||
|
m2 = /^[ \t>+-]*(\* )?/.exec(ln),
|
||||||
|
drop = dom_src.selectionEnd - dom_src.selectionStart;
|
||||||
|
|
||||||
s.pre = s.md.substring(0, s.car) + '\n' + m[0];
|
var pre = m2[0];
|
||||||
|
if (m1 !== null)
|
||||||
|
pre = m1[1] + (parseInt(m1[2]) + 1) + m1[3];
|
||||||
|
|
||||||
|
if (pre.length > s.car - s.n1)
|
||||||
|
// in gutter, do nothing
|
||||||
|
return true;
|
||||||
|
|
||||||
|
s.pre = s.md.substring(0, s.car) + '\n' + pre;
|
||||||
|
s.sel = '';
|
||||||
|
s.post = s.md.substring(s.car + drop);
|
||||||
|
s.car = s.cdr = s.pre.length;
|
||||||
|
setsel(s);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// backspace
|
||||||
|
function md_backspace() {
|
||||||
|
var s = linebounds(true),
|
||||||
|
o0 = dom_src.selectionStart,
|
||||||
|
left = s.md.slice(s.n1, o0),
|
||||||
|
m = /^[ \t>+-]*(\* )?([0-9]+\. +)?/.exec(left);
|
||||||
|
|
||||||
|
// if car is in whitespace area, do nothing
|
||||||
|
if (/^\s*$/.test(left))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// same if line is all-whitespace or non-markup
|
||||||
|
var v = m[0].replace(/[^ ]/g, " ");
|
||||||
|
if (v === m[0] || v.length !== left.length)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
s.pre = s.md.substring(0, s.n1) + v;
|
||||||
s.sel = '';
|
s.sel = '';
|
||||||
s.post = s.md.substring(s.car);
|
s.post = s.md.substring(s.car);
|
||||||
s.car = s.cdr = s.pre.length;
|
s.car = s.cdr = s.pre.length;
|
||||||
setsel(s);
|
setsel(s);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// paragraph jump
|
||||||
|
function md_p_jump(down) {
|
||||||
|
var txt = dom_src.value,
|
||||||
|
ofs = dom_src.selectionStart;
|
||||||
|
|
||||||
|
if (down) {
|
||||||
|
while (txt[ofs] == '\n' && --ofs > 0);
|
||||||
|
ofs = txt.indexOf("\n\n", ofs);
|
||||||
|
if (ofs < 0)
|
||||||
|
ofs = txt.length - 1;
|
||||||
|
|
||||||
|
while (txt[ofs] == '\n' && ++ofs < txt.length - 1);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
txt += '\n\n';
|
||||||
|
while (ofs > 1 && txt[ofs - 1] == '\n') ofs--;
|
||||||
|
ofs = Math.max(0, txt.lastIndexOf("\n\n", ofs - 1));
|
||||||
|
while (txt[ofs] == '\n' && ++ofs < txt.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
dom_src.setSelectionRange(ofs, ofs, "none");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function reLastIndexOf(txt, ptn, end) {
|
||||||
|
var ofs = (typeof end !== 'undefined') ? end : txt.length;
|
||||||
|
end = ofs;
|
||||||
|
while (ofs >= 0) {
|
||||||
|
var sub = txt.slice(ofs, end);
|
||||||
|
if (ptn.test(sub))
|
||||||
|
return ofs;
|
||||||
|
|
||||||
|
ofs--;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// table formatter
|
||||||
|
function fmt_table(e) {
|
||||||
|
if (e) e.preventDefault();
|
||||||
|
//dom_tbox.setAttribute('class', '');
|
||||||
|
|
||||||
|
var txt = dom_src.value,
|
||||||
|
ofs = dom_src.selectionStart,
|
||||||
|
//o0 = txt.lastIndexOf('\n\n', ofs),
|
||||||
|
//o1 = txt.indexOf('\n\n', ofs);
|
||||||
|
o0 = reLastIndexOf(txt, /\n\s*\n/m, ofs),
|
||||||
|
o1 = txt.slice(ofs).search(/\n\s*\n/m);
|
||||||
|
// note \s contains \n but its fine
|
||||||
|
|
||||||
|
if (o0 < 0)
|
||||||
|
o0 = 0;
|
||||||
|
else {
|
||||||
|
// seek past the hit
|
||||||
|
var m = /\n\s*\n/m.exec(txt.slice(o0));
|
||||||
|
o0 += m[0].length;
|
||||||
|
}
|
||||||
|
|
||||||
|
o1 = o1 < 0 ? txt.length : o1 + ofs;
|
||||||
|
|
||||||
|
var err = 'cannot format table due to ',
|
||||||
|
tab = txt.slice(o0, o1).split(/\s*\n/),
|
||||||
|
re_ind = /^\s*/,
|
||||||
|
ind = tab[1].match(re_ind)[0],
|
||||||
|
r0_ind = tab[0].slice(0, ind.length),
|
||||||
|
lpipe = tab[1].indexOf('|') < tab[1].indexOf('-'),
|
||||||
|
rpipe = tab[1].lastIndexOf('|') > tab[1].lastIndexOf('-'),
|
||||||
|
re_lpipe = lpipe ? /^\s*\|\s*/ : /^\s*/,
|
||||||
|
re_rpipe = rpipe ? /\s*\|\s*$/ : /\s*$/;
|
||||||
|
|
||||||
|
for (var a = 0; a < tab.length; a++) {
|
||||||
|
var ind2 = tab[a].match(re_ind)[0];
|
||||||
|
if (ind != ind2 && a > 0) // the table can be a list entry or something, ignore [0]
|
||||||
|
return alert(err + 'indentation mismatch on row 2 and ' + (a + 1) + ',\n' + tab[a]);
|
||||||
|
|
||||||
|
var t = tab[a].slice(ind.length);
|
||||||
|
t = t.replace(re_lpipe, "");
|
||||||
|
t = t.replace(re_rpipe, "");
|
||||||
|
tab[a] = t.split(/\s*\|\s*/g);
|
||||||
|
|
||||||
|
if (a == 0)
|
||||||
|
ncols = tab[a].length;
|
||||||
|
|
||||||
|
if (ncols != tab[a].length)
|
||||||
|
return alert(err + 'num.columns mismatch on row 2 and ' + (a + 1) + '; ' + ncols + ' != ' + tab[a].length);
|
||||||
|
}
|
||||||
|
|
||||||
|
var re_align = /^ *(:?)-+(:?) *$/;
|
||||||
|
var align = [];
|
||||||
|
for (var col = 0; col < tab[1].length; col++) {
|
||||||
|
var m = tab[1][col].match(re_align);
|
||||||
|
if (!m)
|
||||||
|
return alert(err + 'invalid column specification, row 2, col ' + (col + 1) + ', [' + tab[1][col] + ']');
|
||||||
|
|
||||||
|
if (m[2]) {
|
||||||
|
if (m[1])
|
||||||
|
align.push('c');
|
||||||
|
else
|
||||||
|
align.push('r');
|
||||||
|
}
|
||||||
|
else
|
||||||
|
align.push('l');
|
||||||
|
}
|
||||||
|
|
||||||
|
var pad = [];
|
||||||
|
var tmax = 0;
|
||||||
|
for (var col = 0; col < ncols; col++) {
|
||||||
|
var max = 0;
|
||||||
|
for (var row = 0; row < tab.length; row++)
|
||||||
|
max = Math.max(max, tab[row][col].length);
|
||||||
|
|
||||||
|
var s = '';
|
||||||
|
for (var n = 0; n < max; n++)
|
||||||
|
s += ' ';
|
||||||
|
|
||||||
|
pad.push(s);
|
||||||
|
tmax = Math.max(max, tmax);
|
||||||
|
}
|
||||||
|
|
||||||
|
var dashes = '';
|
||||||
|
for (var a = 0; a < tmax; a++)
|
||||||
|
dashes += '-';
|
||||||
|
|
||||||
|
var ret = [];
|
||||||
|
for (var row = 0; row < tab.length; row++) {
|
||||||
|
var ln = [];
|
||||||
|
for (var col = 0; col < tab[row].length; col++) {
|
||||||
|
var p = pad[col];
|
||||||
|
var s = tab[row][col];
|
||||||
|
|
||||||
|
if (align[col] == 'l') {
|
||||||
|
s = (s + p).slice(0, p.length);
|
||||||
|
}
|
||||||
|
else if (align[col] == 'r') {
|
||||||
|
s = (p + s).slice(-p.length);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
var pt = p.length - s.length;
|
||||||
|
var pl = p.slice(0, Math.floor(pt / 2));
|
||||||
|
var pr = p.slice(0, pt - pl.length);
|
||||||
|
s = pl + s + pr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (row == 1) {
|
||||||
|
if (align[col] == 'l')
|
||||||
|
s = dashes.slice(0, p.length);
|
||||||
|
else if (align[col] == 'r')
|
||||||
|
s = dashes.slice(0, p.length - 1) + ':';
|
||||||
|
else
|
||||||
|
s = ':' + dashes.slice(0, p.length - 2) + ':';
|
||||||
|
}
|
||||||
|
ln.push(s);
|
||||||
|
}
|
||||||
|
ret.push(ind + '| ' + ln.join(' | ') + ' |');
|
||||||
|
}
|
||||||
|
|
||||||
|
// restore any markup in the row0 gutter
|
||||||
|
ret[0] = r0_ind + ret[0].slice(ind.length);
|
||||||
|
|
||||||
|
ret = {
|
||||||
|
"pre": txt.slice(0, o0),
|
||||||
|
"sel": ret.join('\n'),
|
||||||
|
"post": txt.slice(o1),
|
||||||
|
"car": o0,
|
||||||
|
"cdr": o0
|
||||||
|
};
|
||||||
|
setsel(ret);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// show unicode
|
||||||
|
function mark_uni(e) {
|
||||||
|
if (e) e.preventDefault();
|
||||||
|
dom_tbox.setAttribute('class', '');
|
||||||
|
|
||||||
|
var txt = dom_src.value,
|
||||||
|
ptn = new RegExp('([^' + js_uni_whitelist + ']+)', 'g');
|
||||||
|
|
||||||
|
mod = txt.replace(/\r/g, "").replace(ptn, "\u2588\u2770$1\u2771");
|
||||||
|
|
||||||
|
if (txt == mod) {
|
||||||
|
alert('no results; no modifications were made');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dom_src.value = mod;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// iterate unicode
|
||||||
|
function iter_uni(e) {
|
||||||
|
if (e) e.preventDefault();
|
||||||
|
|
||||||
|
var txt = dom_src.value,
|
||||||
|
ofs = dom_src.selectionDirection == "forward" ? dom_src.selectionEnd : dom_src.selectionStart,
|
||||||
|
re = new RegExp('([^' + js_uni_whitelist + ']+)'),
|
||||||
|
m = re.exec(txt.slice(ofs));
|
||||||
|
|
||||||
|
if (!m) {
|
||||||
|
alert('no more hits from cursor onwards');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ofs += m.index;
|
||||||
|
|
||||||
|
dom_src.setSelectionRange(ofs, ofs + m[0].length, "forward");
|
||||||
|
dom_src.oninput();
|
||||||
|
// support chrome:
|
||||||
|
dom_src.blur();
|
||||||
|
dom_src.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// configure whitelist
|
||||||
|
function cfg_uni(e) {
|
||||||
|
if (e) e.preventDefault();
|
||||||
|
esc_uni_whitelist = prompt("unicode whitelist", esc_uni_whitelist);
|
||||||
|
js_uni_whitelist = eval('\'' + esc_uni_whitelist + '\'');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -455,6 +785,11 @@ function md_newline() {
|
|||||||
save();
|
save();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (ev.code == "Escape" || kc == 27) {
|
||||||
|
var d = document.getElementById('helpclose');
|
||||||
|
if (d)
|
||||||
|
d.click();
|
||||||
|
}
|
||||||
if (document.activeElement == dom_src) {
|
if (document.activeElement == dom_src) {
|
||||||
if (ev.code == "Tab" || kc == 9) {
|
if (ev.code == "Tab" || kc == 9) {
|
||||||
md_indent(ev.shiftKey);
|
md_indent(ev.shiftKey);
|
||||||
@@ -469,8 +804,7 @@ function md_newline() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!ctrl && !ev.shiftKey && (ev.code == "Enter" || kc == 13)) {
|
if (!ctrl && !ev.shiftKey && (ev.code == "Enter" || kc == 13)) {
|
||||||
md_newline();
|
return md_newline();
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
if (ctrl && (ev.code == "KeyZ" || kc == 90)) {
|
if (ctrl && (ev.code == "KeyZ" || kc == 90)) {
|
||||||
if (ev.shiftKey)
|
if (ev.shiftKey)
|
||||||
@@ -484,6 +818,28 @@ function md_newline() {
|
|||||||
action_stack.redo();
|
action_stack.redo();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (!ctrl && !ev.shiftKey && kc == 8) {
|
||||||
|
return md_backspace();
|
||||||
|
}
|
||||||
|
if (ctrl && (ev.code == "KeyK")) {
|
||||||
|
fmt_table();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (ctrl && (ev.code == "KeyU")) {
|
||||||
|
iter_uni();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (ctrl && (ev.code == "KeyE")) {
|
||||||
|
dom_nsbs.click();
|
||||||
|
//fmt_table();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var up = ev.code == "ArrowUp" || kc == 38;
|
||||||
|
var dn = ev.code == "ArrowDown" || kc == 40;
|
||||||
|
if (ctrl && (up || dn)) {
|
||||||
|
md_p_jump(dn);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
document.onkeydown = keydown;
|
document.onkeydown = keydown;
|
||||||
@@ -491,12 +847,23 @@ function md_newline() {
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
||||||
|
document.getElementById('tools').onclick = function (e) {
|
||||||
|
if (e) e.preventDefault();
|
||||||
|
var is_open = dom_tbox.getAttribute('class') != 'open';
|
||||||
|
dom_tbox.setAttribute('class', is_open ? 'open' : '');
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
document.getElementById('help').onclick = function (e) {
|
document.getElementById('help').onclick = function (e) {
|
||||||
if (e) e.preventDefault();
|
if (e) e.preventDefault();
|
||||||
|
dom_tbox.setAttribute('class', '');
|
||||||
|
|
||||||
var dom = document.getElementById('helpbox');
|
var dom = document.getElementById('helpbox');
|
||||||
var dtxt = dom.getElementsByTagName('textarea');
|
var dtxt = dom.getElementsByTagName('textarea');
|
||||||
if (dtxt.length > 0)
|
if (dtxt.length > 0) {
|
||||||
dom.innerHTML = '<a href="#" id="helpclose">close</a>' + marked(dtxt[0].value);
|
convert_markdown(dtxt[0].value, dom);
|
||||||
|
dom.innerHTML = '<a href="#" id="helpclose">close</a>' + dom.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
dom.style.display = 'block';
|
dom.style.display = 'block';
|
||||||
document.getElementById('helpclose').onclick = function () {
|
document.getElementById('helpclose').onclick = function () {
|
||||||
@@ -505,10 +872,18 @@ document.getElementById('help').onclick = function (e) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
document.getElementById('fmt_table').onclick = fmt_table;
|
||||||
|
document.getElementById('mark_uni').onclick = mark_uni;
|
||||||
|
document.getElementById('iter_uni').onclick = iter_uni;
|
||||||
|
document.getElementById('cfg_uni').onclick = cfg_uni;
|
||||||
|
|
||||||
|
|
||||||
// blame steen
|
// blame steen
|
||||||
action_stack = (function () {
|
action_stack = (function () {
|
||||||
var undos = [];
|
var hist = {
|
||||||
var redos = [];
|
un: [],
|
||||||
|
re: []
|
||||||
|
};
|
||||||
var sched_cpos = 0;
|
var sched_cpos = 0;
|
||||||
var sched_timer = null;
|
var sched_timer = null;
|
||||||
var ignore = false;
|
var ignore = false;
|
||||||
@@ -553,7 +928,7 @@ action_stack = (function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function apply(src, dst) {
|
function apply(src, dst) {
|
||||||
dbg('undos(%d) redos(%d)', undos.length, redos.length);
|
dbg('undos(%d) redos(%d)', hist.un.length, hist.re.length);
|
||||||
|
|
||||||
if (src.length === 0)
|
if (src.length === 0)
|
||||||
return false;
|
return false;
|
||||||
@@ -572,7 +947,7 @@ action_stack = (function () {
|
|||||||
dom_src.value = ref;
|
dom_src.value = ref;
|
||||||
dom_src.setSelectionRange(cpos, cpos);
|
dom_src.setSelectionRange(cpos, cpos);
|
||||||
ignore = true; // all browsers
|
ignore = true; // all browsers
|
||||||
draw_md();
|
dom_src.oninput();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -581,36 +956,36 @@ action_stack = (function () {
|
|||||||
ignore = false;
|
ignore = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
redos = [];
|
hist.re = [];
|
||||||
clearTimeout(sched_timer);
|
clearTimeout(sched_timer);
|
||||||
sched_cpos = dom_src.selectionEnd;
|
sched_cpos = dom_src.selectionEnd;
|
||||||
sched_timer = setTimeout(push, 500);
|
sched_timer = setTimeout(push, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
function undo() {
|
function undo() {
|
||||||
if (redos.length == 0) {
|
if (hist.re.length == 0) {
|
||||||
clearTimeout(sched_timer);
|
clearTimeout(sched_timer);
|
||||||
push();
|
push();
|
||||||
}
|
}
|
||||||
return apply(undos, redos);
|
return apply(hist.un, hist.re);
|
||||||
}
|
}
|
||||||
|
|
||||||
function redo() {
|
function redo() {
|
||||||
return apply(redos, undos);
|
return apply(hist.re, hist.un);
|
||||||
}
|
}
|
||||||
|
|
||||||
function push() {
|
function push() {
|
||||||
var newtxt = dom_src.value;
|
var newtxt = dom_src.value;
|
||||||
var change = diff(ref, newtxt, sched_cpos);
|
var change = diff(ref, newtxt, sched_cpos);
|
||||||
if (change !== null)
|
if (change !== null)
|
||||||
undos.push(change);
|
hist.un.push(change);
|
||||||
|
|
||||||
ref = newtxt;
|
ref = newtxt;
|
||||||
dbg('undos(%d) redos(%d)', undos.length, redos.length);
|
dbg('undos(%d) redos(%d)', hist.un.length, hist.re.length);
|
||||||
if (undos.length > 0)
|
if (hist.un.length > 0)
|
||||||
dbg(undos.slice(-1)[0]);
|
dbg(static(hist.un.slice(-1)[0]));
|
||||||
if (redos.length > 0)
|
if (hist.re.length > 0)
|
||||||
dbg(redos.slice(-1)[0]);
|
dbg(static(hist.re.slice(-1)[0]));
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -618,8 +993,7 @@ action_stack = (function () {
|
|||||||
undo: undo,
|
undo: undo,
|
||||||
redo: redo,
|
redo: redo,
|
||||||
push: schedule_push,
|
push: schedule_push,
|
||||||
_undos: undos,
|
_hist: hist,
|
||||||
_redos: redos,
|
|
||||||
_ref: ref
|
_ref: ref
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
@@ -633,4 +1007,4 @@ document.getElementById('help').onclick = function () {
|
|||||||
if (c1[a] !== c2[a])
|
if (c1[a] !== c2[a])
|
||||||
console.log(c1[a] + '\n' + c2[a]);
|
console.log(c1[a] + '\n' + c2[a]);
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -160,8 +160,12 @@ h2 {
|
|||||||
.mdo ol>li {
|
.mdo ol>li {
|
||||||
margin: .7em 0;
|
margin: .7em 0;
|
||||||
}
|
}
|
||||||
|
strong {
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
p>em,
|
p>em,
|
||||||
li>em {
|
li>em,
|
||||||
|
td>em {
|
||||||
color: #c50;
|
color: #c50;
|
||||||
padding: .1em;
|
padding: .1em;
|
||||||
border-bottom: .1em solid #bbb;
|
border-bottom: .1em solid #bbb;
|
||||||
@@ -253,8 +257,12 @@ html.dark .mdo>ul,
|
|||||||
html.dark .mdo>ol {
|
html.dark .mdo>ol {
|
||||||
border-color: #555;
|
border-color: #555;
|
||||||
}
|
}
|
||||||
|
html.dark strong {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
html.dark p>em,
|
html.dark p>em,
|
||||||
html.dark li>em {
|
html.dark li>em,
|
||||||
|
html.dark td>em {
|
||||||
color: #f94;
|
color: #f94;
|
||||||
border-color: #666;
|
border-color: #666;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="m">
|
<div id="m">
|
||||||
<textarea id="mt" style="display:none">{{ md }}</textarea>
|
<textarea id="mt" style="display:none" autocomplete="off">{{ md }}</textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
@@ -39,6 +39,6 @@ var lightswitch = (function () {
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
<script src="/.cpr/deps/easymde.full.js"></script>
|
<script src="/.cpr/deps/easymde.js"></script>
|
||||||
<script src="/.cpr/mde.js"></script>
|
<script src="/.cpr/mde.js"></script>
|
||||||
</body></html>
|
</body></html>
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ function save(mde) {
|
|||||||
fd.append("lastmod", (force ? -1 : last_modified));
|
fd.append("lastmod", (force ? -1 : last_modified));
|
||||||
fd.append("body", txt);
|
fd.append("body", txt);
|
||||||
|
|
||||||
var url = (document.location + '').split('?')[0] + '?raw';
|
var url = (document.location + '').split('?')[0];
|
||||||
var xhr = new XMLHttpRequest();
|
var xhr = new XMLHttpRequest();
|
||||||
xhr.open('POST', url, true);
|
xhr.open('POST', url, true);
|
||||||
xhr.responseType = 'text';
|
xhr.responseType = 'text';
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ head -c $((2*1024*1024*1024)) /dev/zero | openssl enc -aes-256-ctr -pass pass:hu
|
|||||||
## testing multiple parallel uploads
|
## testing multiple parallel uploads
|
||||||
## usage: para | tee log
|
## usage: para | tee log
|
||||||
|
|
||||||
para() { for s in 1 2 3 4 5 6 7 8 12 16 24 32 48 64; do echo $s; for r in {1..4}; do for ((n=0;n<s;n++)); do curl -sF "act=bput" -F "f=@garbage.file" http://127.0.0.1:1234/ 2>&1 & done; wait; echo; done; done; }
|
para() { for s in 1 2 3 4 5 6 7 8 12 16 24 32 48 64; do echo $s; for r in {1..4}; do for ((n=0;n<s;n++)); do curl -sF "act=bput" -F "f=@garbage.file" http://127.0.0.1:3923/ 2>&1 & done; wait; echo; done; done; }
|
||||||
|
|
||||||
|
|
||||||
##
|
##
|
||||||
@@ -36,13 +36,13 @@ for dir in "${dirs[@]}"; do for fn in ふが "$(printf \\xed\\x93)" 'qwe,rty;asd
|
|||||||
|
|
||||||
fn=$(printf '\xba\xdc\xab.cab')
|
fn=$(printf '\xba\xdc\xab.cab')
|
||||||
echo asdf > "$fn"
|
echo asdf > "$fn"
|
||||||
curl --cookie cppwd=wark -sF "act=bput" -F "f=@$fn" http://127.0.0.1:1234/moji/%ED%91/
|
curl --cookie cppwd=wark -sF "act=bput" -F "f=@$fn" http://127.0.0.1:3923/moji/%ED%91/
|
||||||
|
|
||||||
|
|
||||||
##
|
##
|
||||||
## test compression
|
## test compression
|
||||||
|
|
||||||
wget -S --header='Accept-Encoding: gzip' -U 'MSIE 6.0; SV1' http://127.0.0.1:1234/.cpr/deps/ogv.js -O- | md5sum; p=~ed/dev/copyparty/copyparty/web/deps/ogv.js.gz; md5sum $p; gzip -d < $p | md5sum
|
wget -S --header='Accept-Encoding: gzip' -U 'MSIE 6.0; SV1' http://127.0.0.1:3923/.cpr/deps/ogv.js -O- | md5sum; p=~ed/dev/copyparty/copyparty/web/deps/ogv.js.gz; md5sum $p; gzip -d < $p | md5sum
|
||||||
|
|
||||||
|
|
||||||
##
|
##
|
||||||
@@ -80,3 +80,45 @@ for d in /usr /var; do find $d -type f -size +30M 2>/dev/null; done | while IFS=
|
|||||||
# py2 on osx
|
# py2 on osx
|
||||||
brew install python@2
|
brew install python@2
|
||||||
pip install virtualenv
|
pip install virtualenv
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
## http 206
|
||||||
|
|
||||||
|
# az = abcdefghijklmnopqrstuvwxyz
|
||||||
|
|
||||||
|
printf '%s\r\n' 'GET /az HTTP/1.1' 'Host: ocv.me' 'Range: bytes=5-10' '' | ncat ocv.me 80
|
||||||
|
# Content-Range: bytes 5-10/26
|
||||||
|
# Content-Length: 6
|
||||||
|
# fghijk
|
||||||
|
|
||||||
|
Range: bytes=0-1 "ab" Content-Range: bytes 0-1/26
|
||||||
|
Range: bytes=24-24 "y" Content-Range: bytes 24-24/26
|
||||||
|
Range: bytes=24-25 "yz" Content-Range: bytes 24-25/26
|
||||||
|
Range: bytes=24- "yz" Content-Range: bytes 24-25/26
|
||||||
|
Range: bytes=25-29 "z" Content-Range: bytes 25-25/26
|
||||||
|
Range: bytes=26- Content-Range: bytes */26
|
||||||
|
HTTP/1.1 416 Requested Range Not Satisfiable
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
## md perf
|
||||||
|
|
||||||
|
var tsh = [];
|
||||||
|
function convert_markdown(md_text, dest_dom) {
|
||||||
|
tsh.push(new Date().getTime());
|
||||||
|
while (tsh.length > 10)
|
||||||
|
tsh.shift();
|
||||||
|
if (tsh.length > 1) {
|
||||||
|
var end = tsh.slice(-2);
|
||||||
|
console.log("render", end.pop() - end.pop(), (tsh[tsh.length - 1] - tsh[0]) / (tsh.length - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
## tmpfiles.d meme
|
||||||
|
|
||||||
|
mk() { rm -rf /tmp/foo; sudo -u ed bash -c 'mkdir /tmp/foo; echo hi > /tmp/foo/bar'; }
|
||||||
|
mk && t0="$(date)" && while true; do date -s "$(date '+ 1 hour')"; systemd-tmpfiles --clean; ls -1 /tmp | grep foo || break; done; echo "$t0"
|
||||||
|
mk && sudo -u ed flock /tmp/foo sleep 40 & sleep 1; ps aux | grep -E 'sleep 40$' && t0="$(date)" && for n in {1..40}; do date -s "$(date '+ 1 day')"; systemd-tmpfiles --clean; ls -1 /tmp | grep foo || break; done; echo "$t0"
|
||||||
|
mk && t0="$(date)" && for n in {1..40}; do date -s "$(date '+ 1 day')"; systemd-tmpfiles --clean; ls -1 /tmp | grep foo || break; tar -cf/dev/null /tmp/foo; done; echo "$t0"
|
||||||
|
|||||||
35
docs/pretend-youre-qnap.patch
Normal file
35
docs/pretend-youre-qnap.patch
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py
|
||||||
|
index 2d3c1ad..e1e85a0 100644
|
||||||
|
--- a/copyparty/httpcli.py
|
||||||
|
+++ b/copyparty/httpcli.py
|
||||||
|
@@ -864,6 +864,30 @@ class HttpCli(object):
|
||||||
|
#
|
||||||
|
# send reply
|
||||||
|
|
||||||
|
+ try:
|
||||||
|
+ fakefn = self.conn.hsrv.fakefn
|
||||||
|
+ fakectr = self.conn.hsrv.fakectr
|
||||||
|
+ fakedata = self.conn.hsrv.fakedata
|
||||||
|
+ except:
|
||||||
|
+ fakefn = b''
|
||||||
|
+ fakectr = 0
|
||||||
|
+ fakedata = b''
|
||||||
|
+
|
||||||
|
+ self.log('\n{} {}\n{}'.format(fakefn, fakectr, open_args[0]))
|
||||||
|
+ if fakefn == open_args[0] and fakectr > 0:
|
||||||
|
+ self.reply(fakedata, mime=guess_mime(req_path)[0])
|
||||||
|
+ self.conn.hsrv.fakectr = fakectr - 1
|
||||||
|
+ else:
|
||||||
|
+ with open_func(*open_args) as f:
|
||||||
|
+ fakedata = f.read()
|
||||||
|
+
|
||||||
|
+ self.conn.hsrv.fakefn = open_args[0]
|
||||||
|
+ self.conn.hsrv.fakedata = fakedata
|
||||||
|
+ self.conn.hsrv.fakectr = 15
|
||||||
|
+ self.reply(fakedata, mime=guess_mime(req_path)[0])
|
||||||
|
+
|
||||||
|
+ return True
|
||||||
|
+
|
||||||
|
self.out_headers["Accept-Ranges"] = "bytes"
|
||||||
|
self.send_headers(
|
||||||
|
length=upper - lower,
|
||||||
62
docs/rclone.md
Normal file
62
docs/rclone.md
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# using rclone to mount a remote copyparty server as a local filesystem
|
||||||
|
|
||||||
|
speed estimates with server and client on the same win10 machine:
|
||||||
|
* `1070 MiB/s` with rclone as both server and client
|
||||||
|
* `570 MiB/s` with rclone-client and `copyparty -ed -j16` as server
|
||||||
|
* `220 MiB/s` with rclone-client and `copyparty -ed` as server
|
||||||
|
* `100 MiB/s` with [../bin/copyparty-fuse.py](../bin/copyparty-fuse.py) as client
|
||||||
|
|
||||||
|
when server is on another machine (1gbit LAN),
|
||||||
|
* `75 MiB/s` with [../bin/copyparty-fuse.py](../bin/copyparty-fuse.py) as client
|
||||||
|
* `92 MiB/s` with rclone-client and `copyparty -ed` as server
|
||||||
|
* `103 MiB/s` (connection max) with `copyparty -ed -j16` and all the others
|
||||||
|
|
||||||
|
|
||||||
|
# creating the config file
|
||||||
|
|
||||||
|
if you want to use password auth, add `headers = Cookie,cppwd=fgsfds` below
|
||||||
|
|
||||||
|
|
||||||
|
### on windows clients:
|
||||||
|
```
|
||||||
|
(
|
||||||
|
echo [cpp]
|
||||||
|
echo type = http
|
||||||
|
echo url = http://127.0.0.1:3923/
|
||||||
|
) > %userprofile%\.config\rclone\rclone.conf
|
||||||
|
```
|
||||||
|
|
||||||
|
also install the windows dependencies: [winfsp](https://github.com/billziss-gh/winfsp/releases/latest)
|
||||||
|
|
||||||
|
|
||||||
|
### on unix clients:
|
||||||
|
```
|
||||||
|
cat > ~/.config/rclone/rclone.conf <<'EOF'
|
||||||
|
[cpp]
|
||||||
|
type = http
|
||||||
|
url = http://127.0.0.1:3923/
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
# mounting the copyparty server locally
|
||||||
|
```
|
||||||
|
rclone.exe mount --vfs-cache-max-age 5s --attr-timeout 5s --dir-cache-time 5s cpp: Z:
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
# use rclone as server too, replacing copyparty
|
||||||
|
|
||||||
|
feels out of place but is too good not to mention
|
||||||
|
|
||||||
|
```
|
||||||
|
rclone.exe serve http --read-only .
|
||||||
|
```
|
||||||
|
|
||||||
|
* `webdav` gives write-access but `http` is twice as fast
|
||||||
|
* `ftp` is buggy, avoid
|
||||||
|
|
||||||
|
|
||||||
|
# bugs
|
||||||
|
|
||||||
|
* rclone-client throws an exception if you try to read an empty file (should return zero bytes)
|
||||||
10
docs/unirange.py
Normal file
10
docs/unirange.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
v = "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
|
||||||
|
for v in v.split(","):
|
||||||
|
if "+" in v:
|
||||||
|
v = v.split("+")[1]
|
||||||
|
if "-" in v:
|
||||||
|
lo, hi = v.split("-")
|
||||||
|
else:
|
||||||
|
lo = hi = v
|
||||||
|
for v in range(int(lo, 16), int(hi, 16) + 1):
|
||||||
|
print("{:4x} [{}]".format(v, chr(v)))
|
||||||
@@ -3,7 +3,7 @@ WORKDIR /z
|
|||||||
ENV ver_asmcrypto=2821dd1dedd1196c378f5854037dda5c869313f3 \
|
ENV ver_asmcrypto=2821dd1dedd1196c378f5854037dda5c869313f3 \
|
||||||
ver_markdownit=10.0.0 \
|
ver_markdownit=10.0.0 \
|
||||||
ver_showdown=1.9.1 \
|
ver_showdown=1.9.1 \
|
||||||
ver_marked=1.0.0 \
|
ver_marked=1.1.0 \
|
||||||
ver_ogvjs=1.6.1 \
|
ver_ogvjs=1.6.1 \
|
||||||
ver_mde=2.10.1 \
|
ver_mde=2.10.1 \
|
||||||
ver_codemirror=5.53.2 \
|
ver_codemirror=5.53.2 \
|
||||||
@@ -11,8 +11,11 @@ ENV ver_asmcrypto=2821dd1dedd1196c378f5854037dda5c869313f3 \
|
|||||||
ver_zopfli=1.0.3
|
ver_zopfli=1.0.3
|
||||||
|
|
||||||
|
|
||||||
# download
|
# download;
|
||||||
RUN apk add cmake make g++ git bash npm patch wget tar pigz brotli gzip unzip python3 python3-dev \
|
# the scp url is latin from https://fonts.googleapis.com/css2?family=Source+Code+Pro&display=swap
|
||||||
|
RUN mkdir -p /z/dist/no-pk \
|
||||||
|
&& wget https://fonts.gstatic.com/s/sourcecodepro/v11/HI_SiYsKILxRpg3hIP6sJ7fM7PqlPevW.woff2 -O scp.woff2 \
|
||||||
|
&& apk add cmake make g++ git bash npm patch wget tar pigz brotli gzip unzip python3 python3-dev brotli py3-brotli \
|
||||||
&& wget https://github.com/brion/ogv.js/releases/download/$ver_ogvjs/ogvjs-$ver_ogvjs.zip -O ogvjs.zip \
|
&& wget https://github.com/brion/ogv.js/releases/download/$ver_ogvjs/ogvjs-$ver_ogvjs.zip -O ogvjs.zip \
|
||||||
&& wget https://github.com/asmcrypto/asmcrypto.js/archive/$ver_asmcrypto.tar.gz -O asmcrypto.tgz \
|
&& wget https://github.com/asmcrypto/asmcrypto.js/archive/$ver_asmcrypto.tar.gz -O asmcrypto.tgz \
|
||||||
&& wget https://github.com/markedjs/marked/archive/v$ver_marked.tar.gz -O marked.tgz \
|
&& wget https://github.com/markedjs/marked/archive/v$ver_marked.tar.gz -O marked.tgz \
|
||||||
@@ -36,23 +39,7 @@ RUN apk add cmake make g++ git bash npm patch wget tar pigz brotli gzip unzi
|
|||||||
&& npm install \
|
&& npm install \
|
||||||
&& npm i gulp-cli -g ) \
|
&& npm i gulp-cli -g ) \
|
||||||
&& unzip fontawesome.zip \
|
&& unzip fontawesome.zip \
|
||||||
&& tar -xf zopfli.tgz \
|
&& tar -xf zopfli.tgz
|
||||||
&& mkdir -p /z/dist/no-pk
|
|
||||||
|
|
||||||
|
|
||||||
# uncomment if you wanna test the abandoned markdown converters
|
|
||||||
#ENV build_abandoned=1
|
|
||||||
|
|
||||||
|
|
||||||
RUN [ $build_abandoned ] || exit 0; \
|
|
||||||
git clone --depth 1 --branch $ver_showdown https://github.com/showdownjs/showdown/ \
|
|
||||||
&& wget https://github.com/markdown-it/markdown-it/archive/$ver_markdownit.tar.gz -O markdownit.tgz \
|
|
||||||
&& (cd showdown \
|
|
||||||
&& npm install \
|
|
||||||
&& npm i grunt -g ) \
|
|
||||||
&& (tar -xf markdownit.tgz \
|
|
||||||
&& cd markdown-it-$ver_markdownit \
|
|
||||||
&& npm install )
|
|
||||||
|
|
||||||
|
|
||||||
# build fonttools (which needs zopfli)
|
# build fonttools (which needs zopfli)
|
||||||
@@ -80,31 +67,27 @@ RUN cd ogvjs-$ver_ogvjs \
|
|||||||
&& cp -pv \
|
&& cp -pv \
|
||||||
ogv.js \
|
ogv.js \
|
||||||
ogv-worker-audio.js \
|
ogv-worker-audio.js \
|
||||||
ogv-demuxer-ogg.js \
|
|
||||||
ogv-demuxer-ogg-wasm.js \
|
ogv-demuxer-ogg-wasm.js \
|
||||||
ogv-demuxer-ogg-wasm.wasm \
|
ogv-demuxer-ogg-wasm.wasm \
|
||||||
ogv-demuxer-webm.js \
|
|
||||||
ogv-demuxer-webm-wasm.js \
|
ogv-demuxer-webm-wasm.js \
|
||||||
ogv-demuxer-webm-wasm.wasm \
|
ogv-demuxer-webm-wasm.wasm \
|
||||||
ogv-decoder-audio-opus.js \
|
|
||||||
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.js \
|
|
||||||
ogv-decoder-audio-vorbis-wasm.js \
|
ogv-decoder-audio-vorbis-wasm.js \
|
||||||
ogv-decoder-audio-vorbis-wasm.wasm \
|
ogv-decoder-audio-vorbis-wasm.wasm \
|
||||||
dynamicaudio.swf \
|
|
||||||
/z/dist
|
/z/dist
|
||||||
|
|
||||||
|
# ogv-demuxer-ogg.js \
|
||||||
|
# ogv-demuxer-webm.js \
|
||||||
|
# ogv-decoder-audio-opus.js \
|
||||||
|
# ogv-decoder-audio-vorbis.js \
|
||||||
|
# dynamicaudio.swf \
|
||||||
|
|
||||||
|
|
||||||
# build marked
|
# build marked
|
||||||
RUN wget https://github.com/markedjs/marked/commit/5c166d4164791f643693478e4ac094d63d6e0c9a.patch -O marked-git-1.patch \
|
|
||||||
&& wget https://patch-diff.githubusercontent.com/raw/markedjs/marked/pull/1652.patch -O marked-git-2.patch
|
|
||||||
|
|
||||||
COPY marked.patch /z/
|
COPY marked.patch /z/
|
||||||
COPY marked-ln.patch /z/
|
COPY marked-ln.patch /z/
|
||||||
RUN cd marked-$ver_marked \
|
RUN cd marked-$ver_marked \
|
||||||
&& patch -p1 < /z/marked-git-1.patch \
|
|
||||||
&& patch -p1 < /z/marked-git-2.patch \
|
|
||||||
&& patch -p1 < /z/marked-ln.patch \
|
&& patch -p1 < /z/marked-ln.patch \
|
||||||
&& patch -p1 < /z/marked.patch \
|
&& patch -p1 < /z/marked.patch \
|
||||||
&& npm run build \
|
&& npm run build \
|
||||||
@@ -138,57 +121,10 @@ RUN cd easy-markdown-editor-$ver_mde \
|
|||||||
&& patch -p1 < /z/easymde-ln.patch \
|
&& patch -p1 < /z/easymde-ln.patch \
|
||||||
&& gulp \
|
&& gulp \
|
||||||
&& cp -pv dist/easymde.min.css /z/dist/easymde.css \
|
&& cp -pv dist/easymde.min.css /z/dist/easymde.css \
|
||||||
&& cp -pv dist/easymde.min.js /z/dist/easymde.js \
|
&& cp -pv dist/easymde.min.js /z/dist/easymde.js
|
||||||
&& sed -ri '/pipe.terser/d; /cleanCSS/d' gulpfile.js \
|
|
||||||
&& gulp \
|
|
||||||
&& cp -pv dist/easymde.min.css /z/dist/easymde.full.css \
|
|
||||||
&& cp -pv dist/easymde.min.js /z/dist/easymde.full.js
|
|
||||||
|
|
||||||
|
|
||||||
# build showdown (abandoned; disabled by default)
|
# build fontawesome and scp
|
||||||
COPY showdown.patch /z/
|
|
||||||
RUN [ $build_abandoned ] || exit 0; \
|
|
||||||
cd showdown \
|
|
||||||
&& rm -rf bin dist \
|
|
||||||
# # remove ellipsis plugin \
|
|
||||||
&& rm \
|
|
||||||
src/subParsers/ellipsis.js \
|
|
||||||
test/cases/ellipsis* \
|
|
||||||
# # remove html-to-md converter \
|
|
||||||
&& rm \
|
|
||||||
test/node/testsuite.makemd.js \
|
|
||||||
test/node/showdown.Converter.makeMarkdown.js \
|
|
||||||
# # remove emojis \
|
|
||||||
&& rm src/subParsers/emoji.js \
|
|
||||||
&& awk '/^showdown.helper.emojis/ {o=1} !o; /^\}/ {o=0}' \
|
|
||||||
>f <src/helpers.js \
|
|
||||||
&& mv f src/helpers.js \
|
|
||||||
&& rm -rf test/features/emojis \
|
|
||||||
# # remove ghmentions \
|
|
||||||
&& rm test/features/ghMentions.* \
|
|
||||||
# # remove option descriptions \
|
|
||||||
&& sed -ri '/descri(ption|be): /d' src/options.js \
|
|
||||||
&& patch -p1 < /z/showdown.patch
|
|
||||||
|
|
||||||
RUN [ $build_abandoned ] || exit 0; \
|
|
||||||
cd showdown \
|
|
||||||
&& grunt build \
|
|
||||||
&& sed -ri '/sourceMappingURL=showdown.min.js.map/d' dist/showdown.min.js \
|
|
||||||
&& mv dist/showdown.min.js /z/dist/showdown.js \
|
|
||||||
&& ls -al /z/dist/showdown.js
|
|
||||||
|
|
||||||
|
|
||||||
# build markdownit (abandoned; disabled by default)
|
|
||||||
COPY markdown-it.patch /z/
|
|
||||||
RUN [ $build_abandoned ] || exit 0; \
|
|
||||||
cd markdown-it-$ver_markdownit \
|
|
||||||
&& patch -p1 < /z/markdown-it.patch \
|
|
||||||
&& make browserify \
|
|
||||||
&& cp -pv dist/markdown-it.min.js /z/dist/markdown-it.js \
|
|
||||||
&& cp -pv dist/markdown-it.js /z/dist/markdown-it-full.js
|
|
||||||
|
|
||||||
|
|
||||||
# build fontawesome
|
|
||||||
COPY mini-fa.sh /z
|
COPY mini-fa.sh /z
|
||||||
COPY mini-fa.css /z
|
COPY mini-fa.css /z
|
||||||
RUN /bin/ash /z/mini-fa.sh
|
RUN /bin/ash /z/mini-fa.sh
|
||||||
@@ -203,38 +139,6 @@ RUN cd /z/dist \
|
|||||||
&& rmdir no-pk
|
&& rmdir no-pk
|
||||||
|
|
||||||
|
|
||||||
# showdown: abandoned due to code-blocks in lists failing
|
# git diff -U2 --no-index marked-1.1.0-orig/ marked-1.1.0-edit/ -U2 | sed -r '/^index /d;s`^(diff --git a/)[^/]+/(.* b/)[^/]+/`\1\2`; s`^(---|\+\+\+) ([ab]/)[^/]+/`\1 \2`' > ../dev/copyparty/scripts/deps-docker/marked-ln.patch
|
||||||
# 22770 orig
|
# d=/home/ed/dev/copyparty/scripts/deps-docker/; tar -cf ../x . && ssh root@$bip "cd $d && tar -xv >&2 && make >&2 && tar -cC ../../copyparty/web deps" <../x | (cd ../../copyparty/web/; cat > the.tgz; tar -xvf the.tgz; rm the.tgz)
|
||||||
# 12154 no-emojis
|
|
||||||
# 12134 no-srcmap
|
|
||||||
# 11189 no-descriptions
|
|
||||||
# 11152 no-ellipsis
|
|
||||||
# 10617 no-this.makeMd
|
|
||||||
# 9569 no-extensions
|
|
||||||
# 9537 no-extensions
|
|
||||||
# 9410 no-mentions
|
|
||||||
|
|
||||||
|
|
||||||
# markdown-it: abandoned because no header anchors (and too big)
|
|
||||||
# 32322 107754 orig (wowee)
|
|
||||||
# 19619 21392 71540 less entities
|
|
||||||
|
|
||||||
|
|
||||||
# marked:
|
|
||||||
# 9253 29773 orig
|
|
||||||
# 9159 29633 no copyright (reverted)
|
|
||||||
# 9040 29057 no sanitize
|
|
||||||
# 8870 28631 no email-mangle
|
|
||||||
# so really not worth it, just drop the patch when that stops working
|
|
||||||
|
|
||||||
|
|
||||||
# easymde:
|
|
||||||
# 91836 orig
|
|
||||||
# 88635 no spellcheck
|
|
||||||
# 88392 no urlRE
|
|
||||||
# 85651 less bidi
|
|
||||||
# 82855 less mode meta
|
|
||||||
|
|
||||||
|
|
||||||
# d=/home/ed/dev/copyparty/scripts/deps-docker/; tar -cf ../x . && ssh root@$bip "cd $d && tar -xv >&2 && make >&2 && tar -cC ../../copyparty/web deps" <../x | (cd ../../copyparty/web/; cat > the.tgz; tar -xvf the.tgz)
|
|
||||||
# gzip -dkf ../dev/copyparty/copyparty/web/deps/deps/marked.full.js.gz && diff -NarU2 ../dev/copyparty/copyparty/web/deps/{,deps/}marked.full.js
|
# gzip -dkf ../dev/copyparty/copyparty/web/deps/deps/marked.full.js.gz && diff -NarU2 ../dev/copyparty/copyparty/web/deps/{,deps/}marked.full.js
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ add data-ln="%d" to most tags, %d is the source markdown line
|
|||||||
+ // this.ln will be bumped by recursive calls into this func;
|
+ // this.ln will be bumped by recursive calls into this func;
|
||||||
+ // reset the count and rely on the outermost token's raw only
|
+ // reset the count and rely on the outermost token's raw only
|
||||||
+ ln = this.ln;
|
+ ln = this.ln;
|
||||||
+
|
+
|
||||||
// newline
|
// newline
|
||||||
if (token = this.tokenizer.space(src)) {
|
if (token = this.tokenizer.space(src)) {
|
||||||
src = src.substring(token.raw.length);
|
src = src.substring(token.raw.length);
|
||||||
@@ -180,7 +180,7 @@ diff --git a/src/Parser.js b/src/Parser.js
|
|||||||
+ // similar to tables, writing contents before the <ul> tag
|
+ // similar to tables, writing contents before the <ul> tag
|
||||||
+ // so update the tag attribute as we go
|
+ // so update the tag attribute as we go
|
||||||
+ // (assuming all list entries got tagged with a source-line, probably safe w)
|
+ // (assuming all list entries got tagged with a source-line, probably safe w)
|
||||||
+ body += this.renderer.tag_ln(item.tokens[0].ln).listitem(itemBody, task, checked);
|
+ body += this.renderer.tag_ln((item.tokens[0] || token).ln).listitem(itemBody, task, checked);
|
||||||
}
|
}
|
||||||
|
|
||||||
- out += this.renderer.list(body, ordered, start);
|
- out += this.renderer.list(body, ordered, start);
|
||||||
@@ -234,7 +234,7 @@ diff --git a/src/Renderer.js b/src/Renderer.js
|
|||||||
- return '<pre><code>'
|
- return '<pre><code>'
|
||||||
+ return '<pre' + this.ln + '><code>'
|
+ return '<pre' + this.ln + '><code>'
|
||||||
+ (escaped ? code : escape(code, true))
|
+ (escaped ? code : escape(code, true))
|
||||||
+ '</code></pre>';
|
+ '</code></pre>\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
- return '<pre><code class="'
|
- return '<pre><code class="'
|
||||||
|
|||||||
@@ -1,7 +1,141 @@
|
|||||||
diff -NarU1 marked-1.0.0-orig/src/defaults.js marked-1.0.0-edit/src/defaults.js
|
diff --git a/src/Lexer.js b/src/Lexer.js
|
||||||
--- marked-1.0.0-orig/src/defaults.js 2020-04-21 01:03:48.000000000 +0000
|
--- a/src/Lexer.js
|
||||||
+++ marked-1.0.0-edit/src/defaults.js 2020-04-25 19:16:56.124621393 +0000
|
+++ b/src/Lexer.js
|
||||||
@@ -9,10 +9,6 @@
|
@@ -5,5 +5,5 @@ const { block, inline } = require('./rules.js');
|
||||||
|
/**
|
||||||
|
* smartypants text replacement
|
||||||
|
- */
|
||||||
|
+ *
|
||||||
|
function smartypants(text) {
|
||||||
|
return text
|
||||||
|
@@ -26,5 +26,5 @@ function smartypants(text) {
|
||||||
|
/**
|
||||||
|
* mangle email addresses
|
||||||
|
- */
|
||||||
|
+ *
|
||||||
|
function mangle(text) {
|
||||||
|
let out = '',
|
||||||
|
@@ -439,5 +439,5 @@ module.exports = class Lexer {
|
||||||
|
|
||||||
|
// autolink
|
||||||
|
- if (token = this.tokenizer.autolink(src, mangle)) {
|
||||||
|
+ if (token = this.tokenizer.autolink(src)) {
|
||||||
|
src = src.substring(token.raw.length);
|
||||||
|
tokens.push(token);
|
||||||
|
@@ -446,5 +446,5 @@ module.exports = class Lexer {
|
||||||
|
|
||||||
|
// url (gfm)
|
||||||
|
- if (!inLink && (token = this.tokenizer.url(src, mangle))) {
|
||||||
|
+ if (!inLink && (token = this.tokenizer.url(src))) {
|
||||||
|
src = src.substring(token.raw.length);
|
||||||
|
tokens.push(token);
|
||||||
|
@@ -453,5 +453,5 @@ module.exports = class Lexer {
|
||||||
|
|
||||||
|
// text
|
||||||
|
- if (token = this.tokenizer.inlineText(src, inRawBlock, smartypants)) {
|
||||||
|
+ if (token = this.tokenizer.inlineText(src, inRawBlock)) {
|
||||||
|
src = src.substring(token.raw.length);
|
||||||
|
tokens.push(token);
|
||||||
|
diff --git a/src/Renderer.js b/src/Renderer.js
|
||||||
|
--- a/src/Renderer.js
|
||||||
|
+++ b/src/Renderer.js
|
||||||
|
@@ -140,5 +140,5 @@ module.exports = class Renderer {
|
||||||
|
|
||||||
|
link(href, title, text) {
|
||||||
|
- href = cleanUrl(this.options.sanitize, this.options.baseUrl, href);
|
||||||
|
+ href = cleanUrl(this.options.baseUrl, href);
|
||||||
|
if (href === null) {
|
||||||
|
return text;
|
||||||
|
@@ -153,5 +153,5 @@ module.exports = class Renderer {
|
||||||
|
|
||||||
|
image(href, title, text) {
|
||||||
|
- href = cleanUrl(this.options.sanitize, this.options.baseUrl, href);
|
||||||
|
+ href = cleanUrl(this.options.baseUrl, href);
|
||||||
|
if (href === null) {
|
||||||
|
return text;
|
||||||
|
diff --git a/src/Tokenizer.js b/src/Tokenizer.js
|
||||||
|
--- a/src/Tokenizer.js
|
||||||
|
+++ b/src/Tokenizer.js
|
||||||
|
@@ -287,11 +287,8 @@ module.exports = class Tokenizer {
|
||||||
|
if (cap) {
|
||||||
|
return {
|
||||||
|
- type: this.options.sanitize
|
||||||
|
- ? 'paragraph'
|
||||||
|
- : 'html',
|
||||||
|
+ type: 'html',
|
||||||
|
raw: cap[0],
|
||||||
|
- pre: !this.options.sanitizer
|
||||||
|
- && (cap[1] === 'pre' || cap[1] === 'script' || cap[1] === 'style'),
|
||||||
|
- text: this.options.sanitize ? (this.options.sanitizer ? this.options.sanitizer(cap[0]) : escape(cap[0])) : cap[0]
|
||||||
|
+ pre: cap[1] === 'pre' || cap[1] === 'script' || cap[1] === 'style',
|
||||||
|
+ text: cap[0]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@@ -421,15 +418,9 @@ module.exports = class Tokenizer {
|
||||||
|
|
||||||
|
return {
|
||||||
|
- type: this.options.sanitize
|
||||||
|
- ? 'text'
|
||||||
|
- : 'html',
|
||||||
|
+ type: 'html',
|
||||||
|
raw: cap[0],
|
||||||
|
inLink,
|
||||||
|
inRawBlock,
|
||||||
|
- text: this.options.sanitize
|
||||||
|
- ? (this.options.sanitizer
|
||||||
|
- ? this.options.sanitizer(cap[0])
|
||||||
|
- : escape(cap[0]))
|
||||||
|
- : cap[0]
|
||||||
|
+ text: cap[0]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@@ -550,10 +541,10 @@ module.exports = class Tokenizer {
|
||||||
|
}
|
||||||
|
|
||||||
|
- autolink(src, mangle) {
|
||||||
|
+ autolink(src) {
|
||||||
|
const cap = this.rules.inline.autolink.exec(src);
|
||||||
|
if (cap) {
|
||||||
|
let text, href;
|
||||||
|
if (cap[2] === '@') {
|
||||||
|
- text = escape(this.options.mangle ? mangle(cap[1]) : cap[1]);
|
||||||
|
+ text = escape(cap[1]);
|
||||||
|
href = 'mailto:' + text;
|
||||||
|
} else {
|
||||||
|
@@ -578,10 +569,10 @@ module.exports = class Tokenizer {
|
||||||
|
}
|
||||||
|
|
||||||
|
- url(src, mangle) {
|
||||||
|
+ url(src) {
|
||||||
|
let cap;
|
||||||
|
if (cap = this.rules.inline.url.exec(src)) {
|
||||||
|
let text, href;
|
||||||
|
if (cap[2] === '@') {
|
||||||
|
- text = escape(this.options.mangle ? mangle(cap[0]) : cap[0]);
|
||||||
|
+ text = escape(cap[0]);
|
||||||
|
href = 'mailto:' + text;
|
||||||
|
} else {
|
||||||
|
@@ -615,12 +606,12 @@ module.exports = class Tokenizer {
|
||||||
|
}
|
||||||
|
|
||||||
|
- inlineText(src, inRawBlock, smartypants) {
|
||||||
|
+ inlineText(src, inRawBlock) {
|
||||||
|
const cap = this.rules.inline.text.exec(src);
|
||||||
|
if (cap) {
|
||||||
|
let text;
|
||||||
|
if (inRawBlock) {
|
||||||
|
- text = this.options.sanitize ? (this.options.sanitizer ? this.options.sanitizer(cap[0]) : escape(cap[0])) : cap[0];
|
||||||
|
+ text = cap[0];
|
||||||
|
} else {
|
||||||
|
- text = escape(this.options.smartypants ? smartypants(cap[0]) : cap[0]);
|
||||||
|
+ text = escape(cap[0]);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
diff --git a/src/defaults.js b/src/defaults.js
|
||||||
|
--- a/src/defaults.js
|
||||||
|
+++ b/src/defaults.js
|
||||||
|
@@ -8,12 +8,8 @@ function getDefaults() {
|
||||||
|
highlight: null,
|
||||||
langPrefix: 'language-',
|
langPrefix: 'language-',
|
||||||
- mangle: true,
|
- mangle: true,
|
||||||
pedantic: false,
|
pedantic: false,
|
||||||
@@ -12,10 +146,12 @@ diff -NarU1 marked-1.0.0-orig/src/defaults.js marked-1.0.0-edit/src/defaults.js
|
|||||||
smartLists: false,
|
smartLists: false,
|
||||||
- smartypants: false,
|
- smartypants: false,
|
||||||
tokenizer: null,
|
tokenizer: null,
|
||||||
diff -NarU1 marked-1.0.0-orig/src/helpers.js marked-1.0.0-edit/src/helpers.js
|
walkTokens: null,
|
||||||
--- marked-1.0.0-orig/src/helpers.js 2020-04-21 01:03:48.000000000 +0000
|
diff --git a/src/helpers.js b/src/helpers.js
|
||||||
+++ marked-1.0.0-edit/src/helpers.js 2020-04-25 18:58:43.001320210 +0000
|
--- a/src/helpers.js
|
||||||
@@ -65,16 +65,3 @@
|
+++ b/src/helpers.js
|
||||||
|
@@ -64,18 +64,5 @@ function edit(regex, opt) {
|
||||||
|
const nonWordAndColonTest = /[^\w:]/g;
|
||||||
const originIndependentUrl = /^$|^[a-z][a-z0-9+.-]*:|^[?#]/i;
|
const originIndependentUrl = /^$|^[a-z][a-z0-9+.-]*:|^[?#]/i;
|
||||||
-function cleanUrl(sanitize, base, href) {
|
-function cleanUrl(sanitize, base, href) {
|
||||||
- if (sanitize) {
|
- if (sanitize) {
|
||||||
@@ -33,7 +169,9 @@ diff -NarU1 marked-1.0.0-orig/src/helpers.js marked-1.0.0-edit/src/helpers.js
|
|||||||
- }
|
- }
|
||||||
+function cleanUrl(base, href) {
|
+function cleanUrl(base, href) {
|
||||||
if (base && !originIndependentUrl.test(href)) {
|
if (base && !originIndependentUrl.test(href)) {
|
||||||
@@ -224,8 +211,2 @@
|
href = resolveUrl(base, href);
|
||||||
|
@@ -223,10 +210,4 @@ function findClosingBracket(str, b) {
|
||||||
|
}
|
||||||
|
|
||||||
-function checkSanitizeDeprecation(opt) {
|
-function checkSanitizeDeprecation(opt) {
|
||||||
- if (opt && opt.sanitize && !opt.silent) {
|
- if (opt && opt.sanitize && !opt.silent) {
|
||||||
@@ -42,228 +180,161 @@ diff -NarU1 marked-1.0.0-orig/src/helpers.js marked-1.0.0-edit/src/helpers.js
|
|||||||
-}
|
-}
|
||||||
-
|
-
|
||||||
module.exports = {
|
module.exports = {
|
||||||
@@ -240,4 +221,3 @@
|
escape,
|
||||||
|
@@ -239,5 +220,4 @@ module.exports = {
|
||||||
|
splitCells,
|
||||||
rtrim,
|
rtrim,
|
||||||
- findClosingBracket,
|
- findClosingBracket,
|
||||||
- checkSanitizeDeprecation
|
- checkSanitizeDeprecation
|
||||||
+ findClosingBracket
|
+ findClosingBracket
|
||||||
};
|
};
|
||||||
diff -NarU1 marked-1.0.0-orig/src/Lexer.js marked-1.0.0-edit/src/Lexer.js
|
diff --git a/src/marked.js b/src/marked.js
|
||||||
--- marked-1.0.0-orig/src/Lexer.js 2020-04-21 01:03:48.000000000 +0000
|
--- a/src/marked.js
|
||||||
+++ marked-1.0.0-edit/src/Lexer.js 2020-04-25 22:46:54.107584066 +0000
|
+++ b/src/marked.js
|
||||||
@@ -6,3 +6,3 @@
|
@@ -7,5 +7,4 @@ const Slugger = require('./Slugger.js');
|
||||||
* smartypants text replacement
|
const {
|
||||||
- */
|
|
||||||
+ *
|
|
||||||
function smartypants(text) {
|
|
||||||
@@ -27,3 +27,3 @@
|
|
||||||
* mangle email addresses
|
|
||||||
- */
|
|
||||||
+ *
|
|
||||||
function mangle(text) {
|
|
||||||
@@ -388,3 +388,3 @@
|
|
||||||
// autolink
|
|
||||||
- if (token = this.tokenizer.autolink(src, mangle)) {
|
|
||||||
+ if (token = this.tokenizer.autolink(src)) {
|
|
||||||
src = src.substring(token.raw.length);
|
|
||||||
@@ -395,3 +395,3 @@
|
|
||||||
// url (gfm)
|
|
||||||
- if (!inLink && (token = this.tokenizer.url(src, mangle))) {
|
|
||||||
+ if (!inLink && (token = this.tokenizer.url(src))) {
|
|
||||||
src = src.substring(token.raw.length);
|
|
||||||
@@ -402,3 +402,3 @@
|
|
||||||
// text
|
|
||||||
- if (token = this.tokenizer.inlineText(src, inRawBlock, smartypants)) {
|
|
||||||
+ if (token = this.tokenizer.inlineText(src, inRawBlock)) {
|
|
||||||
src = src.substring(token.raw.length);
|
|
||||||
diff -NarU1 marked-1.0.0-orig/src/marked.js marked-1.0.0-edit/src/marked.js
|
|
||||||
--- marked-1.0.0-orig/src/marked.js 2020-04-21 01:03:48.000000000 +0000
|
|
||||||
+++ marked-1.0.0-edit/src/marked.js 2020-04-25 22:42:55.140924439 +0000
|
|
||||||
@@ -8,3 +8,2 @@
|
|
||||||
merge,
|
merge,
|
||||||
- checkSanitizeDeprecation,
|
- checkSanitizeDeprecation,
|
||||||
escape
|
escape
|
||||||
@@ -37,3 +36,2 @@
|
} = require('./helpers.js');
|
||||||
opt = merge({}, marked.defaults, opt || {});
|
@@ -35,5 +34,4 @@ function marked(src, opt, callback) {
|
||||||
- checkSanitizeDeprecation(opt);
|
|
||||||
const highlight = opt.highlight;
|
opt = merge({}, marked.defaults, opt || {});
|
||||||
@@ -101,6 +99,5 @@
|
- checkSanitizeDeprecation(opt);
|
||||||
opt = merge({}, marked.defaults, opt || {});
|
|
||||||
- checkSanitizeDeprecation(opt);
|
if (callback) {
|
||||||
return Parser.parse(Lexer.lex(src, opt), opt);
|
@@ -108,5 +106,5 @@ function marked(src, opt, callback) {
|
||||||
|
return Parser.parse(tokens, opt);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
- e.message += '\nPlease report this to https://github.com/markedjs/marked.';
|
- e.message += '\nPlease report this to https://github.com/markedjs/marked.';
|
||||||
+ e.message += '\nmake issue @ https://github.com/9001/copyparty';
|
+ e.message += '\nmake issue @ https://github.com/9001/copyparty';
|
||||||
if ((opt || marked.defaults).silent) {
|
if (opt.silent) {
|
||||||
diff -NarU1 marked-1.0.0-orig/src/Renderer.js marked-1.0.0-edit/src/Renderer.js
|
return '<p>An error occurred:</p><pre>'
|
||||||
--- marked-1.0.0-orig/src/Renderer.js 2020-04-21 01:03:48.000000000 +0000
|
diff --git a/test/bench.js b/test/bench.js
|
||||||
+++ marked-1.0.0-edit/src/Renderer.js 2020-04-25 18:59:15.091319265 +0000
|
--- a/test/bench.js
|
||||||
@@ -134,3 +134,3 @@
|
+++ b/test/bench.js
|
||||||
link(href, title, text) {
|
@@ -33,5 +33,4 @@ async function runBench(options) {
|
||||||
- href = cleanUrl(this.options.sanitize, this.options.baseUrl, href);
|
breaks: false,
|
||||||
+ href = cleanUrl(this.options.baseUrl, href);
|
|
||||||
if (href === null) {
|
|
||||||
@@ -147,3 +147,3 @@
|
|
||||||
image(href, title, text) {
|
|
||||||
- href = cleanUrl(this.options.sanitize, this.options.baseUrl, href);
|
|
||||||
+ href = cleanUrl(this.options.baseUrl, href);
|
|
||||||
if (href === null) {
|
|
||||||
diff -NarU1 marked-1.0.0-orig/src/Tokenizer.js marked-1.0.0-edit/src/Tokenizer.js
|
|
||||||
--- marked-1.0.0-orig/src/Tokenizer.js 2020-04-21 01:03:48.000000000 +0000
|
|
||||||
+++ marked-1.0.0-edit/src/Tokenizer.js 2020-04-25 22:47:07.610917004 +0000
|
|
||||||
@@ -256,9 +256,6 @@
|
|
||||||
return {
|
|
||||||
- type: this.options.sanitize
|
|
||||||
- ? 'paragraph'
|
|
||||||
- : 'html',
|
|
||||||
- raw: cap[0],
|
|
||||||
- pre: !this.options.sanitizer
|
|
||||||
- && (cap[1] === 'pre' || cap[1] === 'script' || cap[1] === 'style'),
|
|
||||||
- text: this.options.sanitize ? (this.options.sanitizer ? this.options.sanitizer(cap[0]) : escape(cap[0])) : cap[0]
|
|
||||||
+ type: 'html',
|
|
||||||
+ raw: cap[0],
|
|
||||||
+ pre: cap[1] === 'pre' || cap[1] === 'script' || cap[1] === 'style',
|
|
||||||
+ text: cap[0]
|
|
||||||
};
|
|
||||||
@@ -382,5 +379,3 @@
|
|
||||||
return {
|
|
||||||
- type: this.options.sanitize
|
|
||||||
- ? 'text'
|
|
||||||
- : 'html',
|
|
||||||
+ type: 'html',
|
|
||||||
raw: cap[0],
|
|
||||||
@@ -388,7 +383,3 @@
|
|
||||||
inRawBlock,
|
|
||||||
- text: this.options.sanitize
|
|
||||||
- ? (this.options.sanitizer
|
|
||||||
- ? this.options.sanitizer(cap[0])
|
|
||||||
- : escape(cap[0]))
|
|
||||||
- : cap[0]
|
|
||||||
+ text: cap[0]
|
|
||||||
};
|
|
||||||
@@ -504,3 +495,3 @@
|
|
||||||
|
|
||||||
- autolink(src, mangle) {
|
|
||||||
+ autolink(src) {
|
|
||||||
const cap = this.rules.inline.autolink.exec(src);
|
|
||||||
@@ -509,3 +500,3 @@
|
|
||||||
if (cap[2] === '@') {
|
|
||||||
- text = escape(this.options.mangle ? mangle(cap[1]) : cap[1]);
|
|
||||||
+ text = escape(cap[1]);
|
|
||||||
href = 'mailto:' + text;
|
|
||||||
@@ -532,3 +523,3 @@
|
|
||||||
|
|
||||||
- url(src, mangle) {
|
|
||||||
+ url(src) {
|
|
||||||
let cap;
|
|
||||||
@@ -537,3 +528,3 @@
|
|
||||||
if (cap[2] === '@') {
|
|
||||||
- text = escape(this.options.mangle ? mangle(cap[0]) : cap[0]);
|
|
||||||
+ text = escape(cap[0]);
|
|
||||||
href = 'mailto:' + text;
|
|
||||||
@@ -569,3 +560,3 @@
|
|
||||||
|
|
||||||
- inlineText(src, inRawBlock, smartypants) {
|
|
||||||
+ inlineText(src, inRawBlock) {
|
|
||||||
const cap = this.rules.inline.text.exec(src);
|
|
||||||
@@ -574,5 +565,5 @@
|
|
||||||
if (inRawBlock) {
|
|
||||||
- text = this.options.sanitize ? (this.options.sanitizer ? this.options.sanitizer(cap[0]) : escape(cap[0])) : cap[0];
|
|
||||||
+ text = cap[0];
|
|
||||||
} else {
|
|
||||||
- text = escape(this.options.smartypants ? smartypants(cap[0]) : cap[0]);
|
|
||||||
+ text = escape(cap[0]);
|
|
||||||
}
|
|
||||||
diff -NarU1 marked-1.0.0-orig/test/bench.js marked-1.0.0-edit/test/bench.js
|
|
||||||
--- marked-1.0.0-orig/test/bench.js 2020-04-21 01:03:48.000000000 +0000
|
|
||||||
+++ marked-1.0.0-edit/test/bench.js 2020-04-25 19:02:27.227980287 +0000
|
|
||||||
@@ -34,3 +34,2 @@
|
|
||||||
pedantic: false,
|
pedantic: false,
|
||||||
- sanitize: false,
|
- sanitize: false,
|
||||||
smartLists: false
|
smartLists: false
|
||||||
@@ -46,3 +45,2 @@
|
});
|
||||||
|
@@ -45,5 +44,4 @@ async function runBench(options) {
|
||||||
|
breaks: false,
|
||||||
pedantic: false,
|
pedantic: false,
|
||||||
- sanitize: false,
|
- sanitize: false,
|
||||||
smartLists: false
|
smartLists: false
|
||||||
@@ -59,3 +57,2 @@
|
});
|
||||||
|
@@ -58,5 +56,4 @@ async function runBench(options) {
|
||||||
|
breaks: false,
|
||||||
pedantic: false,
|
pedantic: false,
|
||||||
- sanitize: false,
|
- sanitize: false,
|
||||||
smartLists: false
|
smartLists: false
|
||||||
@@ -71,3 +68,2 @@
|
});
|
||||||
|
@@ -70,5 +67,4 @@ async function runBench(options) {
|
||||||
|
breaks: false,
|
||||||
pedantic: false,
|
pedantic: false,
|
||||||
- sanitize: false,
|
- sanitize: false,
|
||||||
smartLists: false
|
smartLists: false
|
||||||
@@ -84,3 +80,2 @@
|
});
|
||||||
|
@@ -83,5 +79,4 @@ async function runBench(options) {
|
||||||
|
breaks: false,
|
||||||
pedantic: true,
|
pedantic: true,
|
||||||
- sanitize: false,
|
- sanitize: false,
|
||||||
smartLists: false
|
smartLists: false
|
||||||
@@ -96,3 +91,2 @@
|
});
|
||||||
|
@@ -95,5 +90,4 @@ async function runBench(options) {
|
||||||
|
breaks: false,
|
||||||
pedantic: true,
|
pedantic: true,
|
||||||
- sanitize: false,
|
- sanitize: false,
|
||||||
smartLists: false
|
smartLists: false
|
||||||
diff -NarU1 marked-1.0.0-orig/test/specs/run-spec.js marked-1.0.0-edit/test/specs/run-spec.js
|
});
|
||||||
--- marked-1.0.0-orig/test/specs/run-spec.js 2020-04-21 01:03:48.000000000 +0000
|
diff --git a/test/specs/run-spec.js b/test/specs/run-spec.js
|
||||||
+++ marked-1.0.0-edit/test/specs/run-spec.js 2020-04-25 19:05:24.321308408 +0000
|
--- a/test/specs/run-spec.js
|
||||||
@@ -21,6 +21,2 @@
|
+++ b/test/specs/run-spec.js
|
||||||
|
@@ -22,8 +22,4 @@ function runSpecs(title, dir, showCompletionTable, options) {
|
||||||
}
|
}
|
||||||
|
|
||||||
- if (spec.options.sanitizer) {
|
- if (spec.options.sanitizer) {
|
||||||
- // eslint-disable-next-line no-eval
|
- // eslint-disable-next-line no-eval
|
||||||
- spec.options.sanitizer = eval(spec.options.sanitizer);
|
- spec.options.sanitizer = eval(spec.options.sanitizer);
|
||||||
- }
|
- }
|
||||||
|
|
||||||
(spec.only ? fit : (spec.skip ? xit : it))('should ' + passFail + example, async() => {
|
(spec.only ? fit : (spec.skip ? xit : it))('should ' + passFail + example, async() => {
|
||||||
@@ -49,2 +45 @@
|
@@ -53,3 +49,2 @@ runSpecs('Original', './original', false, { gfm: false, pedantic: true });
|
||||||
|
runSpecs('New', './new');
|
||||||
runSpecs('ReDOS', './redos');
|
runSpecs('ReDOS', './redos');
|
||||||
-runSpecs('Security', './security', false, { silent: true }); // silent - do not show deprecation warning
|
-runSpecs('Security', './security', false, { silent: true }); // silent - do not show deprecation warning
|
||||||
diff -NarU1 marked-1.0.0-orig/test/unit/Lexer-spec.js marked-1.0.0-edit/test/unit/Lexer-spec.js
|
diff --git a/test/unit/Lexer-spec.js b/test/unit/Lexer-spec.js
|
||||||
--- marked-1.0.0-orig/test/unit/Lexer-spec.js 2020-04-21 01:03:48.000000000 +0000
|
--- a/test/unit/Lexer-spec.js
|
||||||
+++ marked-1.0.0-edit/test/unit/Lexer-spec.js 2020-04-25 22:47:27.170916427 +0000
|
+++ b/test/unit/Lexer-spec.js
|
||||||
@@ -464,3 +464,3 @@
|
@@ -465,5 +465,5 @@ a | b
|
||||||
|
});
|
||||||
|
|
||||||
- it('sanitize', () => {
|
- it('sanitize', () => {
|
||||||
+ /*it('sanitize', () => {
|
+ /*it('sanitize', () => {
|
||||||
expectTokens({
|
expectTokens({
|
||||||
@@ -482,3 +482,3 @@
|
md: '<div>html</div>',
|
||||||
|
@@ -483,5 +483,5 @@ a | b
|
||||||
|
]
|
||||||
});
|
});
|
||||||
- });
|
- });
|
||||||
+ });*/
|
+ });*/
|
||||||
});
|
});
|
||||||
@@ -586,3 +586,3 @@
|
|
||||||
|
@@ -587,5 +587,5 @@ a | b
|
||||||
|
});
|
||||||
|
|
||||||
- it('html sanitize', () => {
|
- it('html sanitize', () => {
|
||||||
+ /*it('html sanitize', () => {
|
+ /*it('html sanitize', () => {
|
||||||
expectInlineTokens({
|
expectInlineTokens({
|
||||||
@@ -596,3 +596,3 @@
|
md: '<div>html</div>',
|
||||||
|
@@ -597,5 +597,5 @@ a | b
|
||||||
|
]
|
||||||
});
|
});
|
||||||
- });
|
- });
|
||||||
+ });*/
|
+ });*/
|
||||||
|
|
||||||
@@ -825,3 +825,3 @@
|
it('link', () => {
|
||||||
|
@@ -909,5 +909,5 @@ a | b
|
||||||
|
});
|
||||||
|
|
||||||
- it('autolink mangle email', () => {
|
- it('autolink mangle email', () => {
|
||||||
+ /*it('autolink mangle email', () => {
|
+ /*it('autolink mangle email', () => {
|
||||||
expectInlineTokens({
|
expectInlineTokens({
|
||||||
@@ -845,3 +845,3 @@
|
md: '<test@example.com>',
|
||||||
|
@@ -929,5 +929,5 @@ a | b
|
||||||
|
]
|
||||||
});
|
});
|
||||||
- });
|
- });
|
||||||
+ });*/
|
+ });*/
|
||||||
|
|
||||||
@@ -882,3 +882,3 @@
|
it('url', () => {
|
||||||
|
@@ -966,5 +966,5 @@ a | b
|
||||||
|
});
|
||||||
|
|
||||||
- it('url mangle email', () => {
|
- it('url mangle email', () => {
|
||||||
+ /*it('url mangle email', () => {
|
+ /*it('url mangle email', () => {
|
||||||
expectInlineTokens({
|
expectInlineTokens({
|
||||||
@@ -902,3 +902,3 @@
|
md: 'test@example.com',
|
||||||
|
@@ -986,5 +986,5 @@ a | b
|
||||||
|
]
|
||||||
});
|
});
|
||||||
- });
|
- });
|
||||||
+ });*/
|
+ });*/
|
||||||
});
|
});
|
||||||
@@ -918,3 +918,3 @@
|
|
||||||
|
@@ -1002,5 +1002,5 @@ a | b
|
||||||
|
});
|
||||||
|
|
||||||
- describe('smartypants', () => {
|
- describe('smartypants', () => {
|
||||||
+ /*describe('smartypants', () => {
|
+ /*describe('smartypants', () => {
|
||||||
it('single quotes', () => {
|
it('single quotes', () => {
|
||||||
@@ -988,3 +988,3 @@
|
expectInlineTokens({
|
||||||
|
@@ -1072,5 +1072,5 @@ a | b
|
||||||
|
});
|
||||||
});
|
});
|
||||||
- });
|
- });
|
||||||
+ });*/
|
+ });*/
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -26,3 +26,6 @@ awk '/:before .content:"\\/ {sub(/[^"]+"./,""); sub(/".*/,""); print}' </z/dist/
|
|||||||
|
|
||||||
# and finally create a woff with just our icons
|
# and finally create a woff with just our icons
|
||||||
pyftsubset "$orig_woff" --unicodes-file=/z/icon.list --no-ignore-missing-unicodes --flavor=woff --with-zopfli --output-file=/z/dist/no-pk/mini-fa.woff --verbose
|
pyftsubset "$orig_woff" --unicodes-file=/z/icon.list --no-ignore-missing-unicodes --flavor=woff --with-zopfli --output-file=/z/dist/no-pk/mini-fa.woff --verbose
|
||||||
|
|
||||||
|
# scp is easier, just want basic latin
|
||||||
|
pyftsubset /z/scp.woff2 --unicodes="20-7e,ab,b7,bb,2022" --no-ignore-missing-unicodes --flavor=woff2 --output-file=/z/dist/no-pk/scp.woff2 --verbose
|
||||||
|
|||||||
100
scripts/fusefuzz.py
Executable file
100
scripts/fusefuzz.py
Executable file
@@ -0,0 +1,100 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
|
"""
|
||||||
|
mkdir -p /dev/shm/fusefuzz/{r,v}
|
||||||
|
PYTHONPATH=.. python3 -m copyparty -v /dev/shm/fusefuzz/r::r -i 127.0.0.1
|
||||||
|
../bin/copyparty-fuse.py /dev/shm/fusefuzz/v http://127.0.0.1:3923/ 2 0
|
||||||
|
(d="$PWD"; cd /dev/shm/fusefuzz && "$d"/fusefuzz.py)
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def chk(fsz, rsz, ofs0, shift, ofs, rf, vf):
|
||||||
|
if ofs != rf.tell():
|
||||||
|
rf.seek(ofs)
|
||||||
|
vf.seek(ofs)
|
||||||
|
|
||||||
|
rb = rf.read(rsz)
|
||||||
|
vb = vf.read(rsz)
|
||||||
|
|
||||||
|
print(f"fsz {fsz} rsz {rsz} ofs {ofs0} shift {shift} ofs {ofs} = {len(rb)}")
|
||||||
|
|
||||||
|
if rb != vb:
|
||||||
|
for n, buf in enumerate([rb, vb]):
|
||||||
|
with open("buf." + str(n), "wb") as f:
|
||||||
|
f.write(buf)
|
||||||
|
|
||||||
|
raise Exception(f"{len(rb)} != {len(vb)}")
|
||||||
|
|
||||||
|
return rb, vb
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
v = "v"
|
||||||
|
for n in range(5):
|
||||||
|
with open(f"r/{n}", "wb") as f:
|
||||||
|
f.write(b"h" * n)
|
||||||
|
|
||||||
|
rand = os.urandom(7919) # prime
|
||||||
|
for fsz in range(1024 * 1024 * 2 - 3, 1024 * 1024 * 2 + 3):
|
||||||
|
with open("r/f", "wb", fsz) as f:
|
||||||
|
f.write((rand * int(fsz / len(rand) + 1))[:fsz])
|
||||||
|
|
||||||
|
for rsz in range(64 * 1024 - 2, 64 * 1024 + 2):
|
||||||
|
ofslist = [0, 1, 2]
|
||||||
|
for n in range(3):
|
||||||
|
ofslist.append(fsz - n)
|
||||||
|
ofslist.append(fsz - (rsz * 1 + n))
|
||||||
|
ofslist.append(fsz - (rsz * 2 + n))
|
||||||
|
|
||||||
|
for ofs0 in ofslist:
|
||||||
|
for shift in range(-3, 3):
|
||||||
|
print(f"fsz {fsz} rsz {rsz} ofs {ofs0} shift {shift}")
|
||||||
|
ofs = ofs0
|
||||||
|
if ofs < 0 or ofs >= fsz:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for n in range(1, 3):
|
||||||
|
with open(f"{v}/{n}", "rb") as f:
|
||||||
|
f.read()
|
||||||
|
|
||||||
|
prev_ofs = -99
|
||||||
|
with open("r/f", "rb", rsz) as rf:
|
||||||
|
with open(f"{v}/f", "rb", rsz) as vf:
|
||||||
|
while True:
|
||||||
|
ofs += shift
|
||||||
|
if ofs < 0 or ofs > fsz or ofs == prev_ofs:
|
||||||
|
break
|
||||||
|
|
||||||
|
prev_ofs = ofs
|
||||||
|
|
||||||
|
rb, vb = chk(fsz, rsz, ofs0, shift, ofs, rf, vf)
|
||||||
|
|
||||||
|
if not rb:
|
||||||
|
break
|
||||||
|
|
||||||
|
ofs += len(rb)
|
||||||
|
|
||||||
|
for n in range(1, 3):
|
||||||
|
with open(f"{v}/{n}", "rb") as f:
|
||||||
|
f.read()
|
||||||
|
|
||||||
|
with open("r/f", "rb", rsz) as rf:
|
||||||
|
with open(f"{v}/f", "rb", rsz) as vf:
|
||||||
|
for n in range(2):
|
||||||
|
ofs += shift
|
||||||
|
if ofs < 0 or ofs > fsz:
|
||||||
|
break
|
||||||
|
|
||||||
|
rb, vb = chk(fsz, rsz, ofs0, shift, ofs, rf, vf)
|
||||||
|
|
||||||
|
ofs -= rsz
|
||||||
|
|
||||||
|
# bumping fsz, sleep away the dentry cache in cppf
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -166,3 +166,6 @@ chmod 755 $sfx_out.*
|
|||||||
printf "done:\n"
|
printf "done:\n"
|
||||||
printf " %s\n" "$(realpath $sfx_out)."{sh,py}
|
printf " %s\n" "$(realpath $sfx_out)."{sh,py}
|
||||||
# rm -rf *
|
# rm -rf *
|
||||||
|
|
||||||
|
# tar -tvf ../sfx/tar | sed -r 's/(.* ....-..-.. ..:.. )(.*)/\2 `` \1/' | sort | sed -r 's/(.*) `` (.*)/\2 \1/'| less
|
||||||
|
# for n in {1..9}; do tar -tf tar | grep -vE '/$' | sed -r 's/(.*)\.(.*)/\2.\1/' | sort | sed -r 's/([^\.]+)\.(.*)/\2.\1/' | tar -cT- | bzip2 -c$n | wc -c; done
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
from __future__ import print_function, unicode_literals
|
from __future__ import print_function, unicode_literals
|
||||||
|
|
||||||
import re, os, sys, stat, time, shutil, tarfile, hashlib, platform, tempfile
|
import re, os, sys, time, shutil, signal, tarfile, hashlib, platform, tempfile
|
||||||
import subprocess as sp
|
import subprocess as sp
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@@ -29,6 +29,7 @@ STAMP = None
|
|||||||
PY2 = sys.version_info[0] == 2
|
PY2 = sys.version_info[0] == 2
|
||||||
sys.dont_write_bytecode = True
|
sys.dont_write_bytecode = True
|
||||||
me = os.path.abspath(os.path.realpath(__file__))
|
me = os.path.abspath(os.path.realpath(__file__))
|
||||||
|
cpp = None
|
||||||
|
|
||||||
|
|
||||||
def eprint(*args, **kwargs):
|
def eprint(*args, **kwargs):
|
||||||
@@ -191,6 +192,16 @@ def makesfx(tar_src, ver, ts):
|
|||||||
# skip 0
|
# skip 0
|
||||||
|
|
||||||
|
|
||||||
|
def u8(gen):
|
||||||
|
try:
|
||||||
|
for s in gen:
|
||||||
|
yield s.decode("utf-8", "ignore")
|
||||||
|
except:
|
||||||
|
yield s
|
||||||
|
for s in gen:
|
||||||
|
yield s
|
||||||
|
|
||||||
|
|
||||||
def get_py_win(ret):
|
def get_py_win(ret):
|
||||||
tops = []
|
tops = []
|
||||||
p = str(os.getenv("LocalAppdata"))
|
p = str(os.getenv("LocalAppdata"))
|
||||||
@@ -216,11 +227,11 @@ def get_py_win(ret):
|
|||||||
# $WIRESHARK_SLOGAN
|
# $WIRESHARK_SLOGAN
|
||||||
for top in tops:
|
for top in tops:
|
||||||
try:
|
try:
|
||||||
for name1 in sorted(os.listdir(top), reverse=True):
|
for name1 in u8(sorted(os.listdir(top), reverse=True)):
|
||||||
if name1.lower().startswith("python"):
|
if name1.lower().startswith("python"):
|
||||||
path1 = os.path.join(top, name1)
|
path1 = os.path.join(top, name1)
|
||||||
try:
|
try:
|
||||||
for name2 in os.listdir(path1):
|
for name2 in u8(os.listdir(path1)):
|
||||||
if name2.lower() == "python.exe":
|
if name2.lower() == "python.exe":
|
||||||
path2 = os.path.join(path1, name2)
|
path2 = os.path.join(path1, name2)
|
||||||
ret[path2.lower()] = path2
|
ret[path2.lower()] = path2
|
||||||
@@ -237,7 +248,7 @@ def get_py_nix(ret):
|
|||||||
next
|
next
|
||||||
|
|
||||||
try:
|
try:
|
||||||
for fn in os.listdir(bindir):
|
for fn in u8(os.listdir(bindir)):
|
||||||
if ptn.match(fn):
|
if ptn.match(fn):
|
||||||
fn = os.path.join(bindir, fn)
|
fn = os.path.join(bindir, fn)
|
||||||
ret[fn.lower()] = fn
|
ret[fn.lower()] = fn
|
||||||
@@ -295,17 +306,19 @@ def hashfile(fn):
|
|||||||
def unpack():
|
def unpack():
|
||||||
"""unpacks the tar yielded by `data`"""
|
"""unpacks the tar yielded by `data`"""
|
||||||
name = "pe-copyparty"
|
name = "pe-copyparty"
|
||||||
|
tag = "v" + str(STAMP)
|
||||||
withpid = "{}.{}".format(name, os.getpid())
|
withpid = "{}.{}".format(name, os.getpid())
|
||||||
top = tempfile.gettempdir()
|
top = tempfile.gettempdir()
|
||||||
final = os.path.join(top, name)
|
final = os.path.join(top, name)
|
||||||
mine = os.path.join(top, withpid)
|
mine = os.path.join(top, withpid)
|
||||||
tar = os.path.join(mine, "tar")
|
tar = os.path.join(mine, "tar")
|
||||||
tag_mine = os.path.join(mine, "v" + str(STAMP))
|
|
||||||
tag_final = os.path.join(final, "v" + str(STAMP))
|
|
||||||
|
|
||||||
if os.path.exists(tag_final):
|
try:
|
||||||
msg("found early")
|
if tag in os.listdir(final):
|
||||||
return final
|
msg("found early")
|
||||||
|
return final
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
nwrite = 0
|
nwrite = 0
|
||||||
os.mkdir(mine)
|
os.mkdir(mine)
|
||||||
@@ -328,12 +341,15 @@ def unpack():
|
|||||||
|
|
||||||
os.remove(tar)
|
os.remove(tar)
|
||||||
|
|
||||||
with open(tag_mine, "wb") as f:
|
with open(os.path.join(mine, tag), "wb") as f:
|
||||||
f.write(b"h\n")
|
f.write(b"h\n")
|
||||||
|
|
||||||
if os.path.exists(tag_final):
|
try:
|
||||||
msg("found late")
|
if tag in os.listdir(final):
|
||||||
return final
|
msg("found late")
|
||||||
|
return final
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if os.path.islink(final):
|
if os.path.islink(final):
|
||||||
@@ -352,7 +368,7 @@ def unpack():
|
|||||||
msg("reloc fail,", mine)
|
msg("reloc fail,", mine)
|
||||||
return mine
|
return mine
|
||||||
|
|
||||||
for fn in os.listdir(top):
|
for fn in u8(os.listdir(top)):
|
||||||
if fn.startswith(name) and fn not in [name, withpid]:
|
if fn.startswith(name) and fn not in [name, withpid]:
|
||||||
try:
|
try:
|
||||||
old = os.path.join(top, fn)
|
old = os.path.join(top, fn)
|
||||||
@@ -418,17 +434,35 @@ def get_payload():
|
|||||||
def confirm():
|
def confirm():
|
||||||
msg()
|
msg()
|
||||||
msg("*** hit enter to exit ***")
|
msg("*** hit enter to exit ***")
|
||||||
raw_input() if PY2 else input()
|
try:
|
||||||
|
raw_input() if PY2 else input()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def run(tmp, py):
|
def run(tmp, py):
|
||||||
|
global cpp
|
||||||
|
|
||||||
msg("OK")
|
msg("OK")
|
||||||
msg("will use:", py)
|
msg("will use:", py)
|
||||||
msg("bound to:", tmp)
|
msg("bound to:", tmp)
|
||||||
|
|
||||||
|
# "systemd-tmpfiles-clean.timer"?? HOW do you even come up with this shit
|
||||||
|
try:
|
||||||
|
import fcntl
|
||||||
|
|
||||||
|
fd = os.open(tmp, os.O_RDONLY)
|
||||||
|
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||||
|
tmp = os.readlink(tmp) # can't flock a symlink, even with O_NOFOLLOW
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
fp_py = os.path.join(tmp, "py")
|
fp_py = os.path.join(tmp, "py")
|
||||||
with open(fp_py, "wb") as f:
|
try:
|
||||||
f.write(py.encode("utf-8") + b"\n")
|
with open(fp_py, "wb") as f:
|
||||||
|
f.write(py.encode("utf-8") + b"\n")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
# avoid loading ./copyparty.py
|
# avoid loading ./copyparty.py
|
||||||
cmd = [
|
cmd = [
|
||||||
@@ -440,16 +474,21 @@ def run(tmp, py):
|
|||||||
] + list(sys.argv[1:])
|
] + list(sys.argv[1:])
|
||||||
|
|
||||||
msg("\n", cmd, "\n")
|
msg("\n", cmd, "\n")
|
||||||
p = sp.Popen(str(x) for x in cmd)
|
cpp = sp.Popen(str(x) for x in cmd)
|
||||||
try:
|
try:
|
||||||
p.wait()
|
cpp.wait()
|
||||||
except:
|
except:
|
||||||
p.wait()
|
cpp.wait()
|
||||||
|
|
||||||
if p.returncode != 0:
|
if cpp.returncode != 0:
|
||||||
confirm()
|
confirm()
|
||||||
|
|
||||||
sys.exit(p.returncode)
|
sys.exit(cpp.returncode)
|
||||||
|
|
||||||
|
|
||||||
|
def bye(sig, frame):
|
||||||
|
if cpp is not None:
|
||||||
|
cpp.terminate()
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
@@ -484,6 +523,8 @@ def main():
|
|||||||
|
|
||||||
# skip 0
|
# skip 0
|
||||||
|
|
||||||
|
signal.signal(signal.SIGTERM, bye)
|
||||||
|
|
||||||
tmp = unpack()
|
tmp = unpack()
|
||||||
fp_py = os.path.join(tmp, "py")
|
fp_py = os.path.join(tmp, "py")
|
||||||
if os.path.exists(fp_py):
|
if os.path.exists(fp_py):
|
||||||
|
|||||||
@@ -32,8 +32,12 @@ dir="$(
|
|||||||
|
|
||||||
# detect available pythons
|
# detect available pythons
|
||||||
(IFS=:; for d in $PATH; do
|
(IFS=:; for d in $PATH; do
|
||||||
printf '%s\n' "$d"/python* "$d"/pypy* | tac;
|
printf '%s\n' "$d"/python* "$d"/pypy*;
|
||||||
done) | grep -E '(python|pypy)[0-9\.-]*$' > $dir/pys || true
|
done) |
|
||||||
|
(sed -E 's/(.*\/[^/0-9]+)([0-9]?[^/]*)$/\2 \1/' || cat) |
|
||||||
|
(sort -nr || cat) |
|
||||||
|
(sed -E 's/([^ ]*) (.*)/\2\1/' || cat) |
|
||||||
|
grep -E '/(python|pypy)[0-9\.-]*$' >$dir/pys || true
|
||||||
|
|
||||||
# see if we made a choice before
|
# see if we made a choice before
|
||||||
[ -z "$pybin" ] && pybin="$(cat $dir/py 2>/dev/null || true)"
|
[ -z "$pybin" ] && pybin="$(cat $dir/py 2>/dev/null || true)"
|
||||||
|
|||||||
164
scripts/speedtest-fs.py
Normal file
164
scripts/speedtest-fs.py
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import stat
|
||||||
|
import time
|
||||||
|
import signal
|
||||||
|
import traceback
|
||||||
|
import threading
|
||||||
|
from queue import Queue
|
||||||
|
|
||||||
|
|
||||||
|
"""speedtest-fs: filesystem performance estimate"""
|
||||||
|
__author__ = "ed <copyparty@ocv.me>"
|
||||||
|
__copyright__ = 2020
|
||||||
|
__license__ = "MIT"
|
||||||
|
__url__ = "https://github.com/9001/copyparty/"
|
||||||
|
|
||||||
|
|
||||||
|
def get_spd(nbyte, nsec):
|
||||||
|
if not nsec:
|
||||||
|
return "0.000 MB 0.000 sec 0.000 MB/s"
|
||||||
|
|
||||||
|
mb = nbyte / (1024 * 1024.0)
|
||||||
|
spd = mb / nsec
|
||||||
|
|
||||||
|
return f"{mb:.3f} MB {nsec:.3f} sec {spd:.3f} MB/s"
|
||||||
|
|
||||||
|
|
||||||
|
class Inf(object):
|
||||||
|
def __init__(self, t0):
|
||||||
|
self.msgs = []
|
||||||
|
self.errors = []
|
||||||
|
self.reports = []
|
||||||
|
self.mtx_msgs = threading.Lock()
|
||||||
|
self.mtx_reports = threading.Lock()
|
||||||
|
|
||||||
|
self.n_byte = 0
|
||||||
|
self.n_sec = 0
|
||||||
|
self.n_done = 0
|
||||||
|
self.t0 = t0
|
||||||
|
|
||||||
|
thr = threading.Thread(target=self.print_msgs)
|
||||||
|
thr.daemon = True
|
||||||
|
thr.start()
|
||||||
|
|
||||||
|
def msg(self, fn, n_read):
|
||||||
|
with self.mtx_msgs:
|
||||||
|
self.msgs.append(f"{fn} {n_read}")
|
||||||
|
|
||||||
|
def err(self, fn):
|
||||||
|
with self.mtx_reports:
|
||||||
|
self.errors.append(f"{fn}\n{traceback.format_exc()}")
|
||||||
|
|
||||||
|
def print_msgs(self):
|
||||||
|
while True:
|
||||||
|
time.sleep(0.02)
|
||||||
|
with self.mtx_msgs:
|
||||||
|
msgs = self.msgs
|
||||||
|
self.msgs = []
|
||||||
|
|
||||||
|
if not msgs:
|
||||||
|
continue
|
||||||
|
|
||||||
|
msgs = msgs[-64:]
|
||||||
|
msgs = [f"{get_spd(self.n_byte, self.n_sec)} {x}" for x in msgs]
|
||||||
|
print("\n".join(msgs))
|
||||||
|
|
||||||
|
def report(self, fn, n_byte, n_sec):
|
||||||
|
with self.mtx_reports:
|
||||||
|
self.reports.append([n_byte, n_sec, fn])
|
||||||
|
self.n_byte += n_byte
|
||||||
|
self.n_sec += n_sec
|
||||||
|
|
||||||
|
def done(self):
|
||||||
|
with self.mtx_reports:
|
||||||
|
self.n_done += 1
|
||||||
|
|
||||||
|
|
||||||
|
def get_files(dir_path):
|
||||||
|
for fn in os.listdir(dir_path):
|
||||||
|
fn = os.path.join(dir_path, fn)
|
||||||
|
st = os.stat(fn).st_mode
|
||||||
|
|
||||||
|
if stat.S_ISDIR(st):
|
||||||
|
yield from get_files(fn)
|
||||||
|
|
||||||
|
if stat.S_ISREG(st):
|
||||||
|
yield fn
|
||||||
|
|
||||||
|
|
||||||
|
def worker(q, inf, read_sz):
|
||||||
|
while True:
|
||||||
|
fn = q.get()
|
||||||
|
if not fn:
|
||||||
|
break
|
||||||
|
|
||||||
|
n_read = 0
|
||||||
|
try:
|
||||||
|
t0 = time.time()
|
||||||
|
with open(fn, "rb") as f:
|
||||||
|
while True:
|
||||||
|
buf = f.read(read_sz)
|
||||||
|
if not buf:
|
||||||
|
break
|
||||||
|
|
||||||
|
n_read += len(buf)
|
||||||
|
inf.msg(fn, n_read)
|
||||||
|
|
||||||
|
inf.report(fn, n_read, time.time() - t0)
|
||||||
|
except:
|
||||||
|
inf.err(fn)
|
||||||
|
|
||||||
|
inf.done()
|
||||||
|
|
||||||
|
|
||||||
|
def sighandler(signo, frame):
|
||||||
|
os._exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
signal.signal(signal.SIGINT, sighandler)
|
||||||
|
|
||||||
|
root = "."
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
root = sys.argv[1]
|
||||||
|
|
||||||
|
t0 = time.time()
|
||||||
|
q = Queue(256)
|
||||||
|
inf = Inf(t0)
|
||||||
|
|
||||||
|
num_threads = 8
|
||||||
|
read_sz = 32 * 1024
|
||||||
|
for _ in range(num_threads):
|
||||||
|
thr = threading.Thread(target=worker, args=(q, inf, read_sz,))
|
||||||
|
thr.daemon = True
|
||||||
|
thr.start()
|
||||||
|
|
||||||
|
for fn in get_files(root):
|
||||||
|
q.put(fn)
|
||||||
|
|
||||||
|
for _ in range(num_threads):
|
||||||
|
q.put(None)
|
||||||
|
|
||||||
|
while inf.n_done < num_threads:
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
t2 = time.time()
|
||||||
|
print("\n")
|
||||||
|
|
||||||
|
log = inf.reports
|
||||||
|
log.sort()
|
||||||
|
for nbyte, nsec, fn in log[-64:]:
|
||||||
|
print(f"{get_spd(nbyte, nsec)} {fn}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("\n".join(inf.errors))
|
||||||
|
|
||||||
|
print(get_spd(inf.n_byte, t2 - t0))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
||||||
26
srv/test.md
26
srv/test.md
@@ -1,5 +1,16 @@
|
|||||||
### hello world
|
### hello world
|
||||||
|
|
||||||
|
* qwe
|
||||||
|
* asd
|
||||||
|
* zxc
|
||||||
|
* 573
|
||||||
|
* one
|
||||||
|
* two
|
||||||
|
|
||||||
|
* |||
|
||||||
|
|--|--|
|
||||||
|
|listed|table|
|
||||||
|
|
||||||
```
|
```
|
||||||
[72....................................................................]
|
[72....................................................................]
|
||||||
[80............................................................................]
|
[80............................................................................]
|
||||||
@@ -17,6 +28,16 @@
|
|||||||
[80............................................................................]
|
[80............................................................................]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
l[i]=1I;(){}o0O</> var foo = "$(`bar`)"; a's'd
|
||||||
|
```
|
||||||
|
|
||||||
|
🔍🌽.📕.🍙🔎
|
||||||
|
|
||||||
|
[](#s1)
|
||||||
|
[s1](#s1)
|
||||||
|
[#s1](#s1)
|
||||||
|
|
||||||
a123456789b123456789c123456789d123456789e123456789f123456789g123456789h123456789i123456789j123456789k123456789l123456789m123456789n123456789o123456789p123456789q123456789r123456789s123456789t123456789u123456789v123456789w123456789x123456789y123456789z123456789
|
a123456789b123456789c123456789d123456789e123456789f123456789g123456789h123456789i123456789j123456789k123456789l123456789m123456789n123456789o123456789p123456789q123456789r123456789s123456789t123456789u123456789v123456789w123456789x123456789y123456789z123456789
|
||||||
|
|
||||||
<foo> bar & <span>baz</span>
|
<foo> bar & <span>baz</span>
|
||||||
@@ -113,6 +134,11 @@ a newline toplevel
|
|||||||
| a table | on the right |
|
| a table | on the right |
|
||||||
| second row | foo bar |
|
| second row | foo bar |
|
||||||
|
|
||||||
|
||
|
||||||
|
--|:-:|-:
|
||||||
|
a table | big text in this | aaakbfddd
|
||||||
|
second row | centred | bbb
|
||||||
|
|
||||||
* list entry
|
* list entry
|
||||||
* [x] yes
|
* [x] yes
|
||||||
* [ ] no
|
* [ ] no
|
||||||
|
|||||||
Reference in New Issue
Block a user