mirror of
https://github.com/9001/copyparty.git
synced 2025-11-05 06:13:20 +00:00
Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4bb4e3a73 | ||
|
|
d25612d038 | ||
|
|
116b2351b0 | ||
|
|
69b83dfdc4 | ||
|
|
3b1839c2ce | ||
|
|
13742ebdf8 | ||
|
|
634657bea1 | ||
|
|
46e70d50b7 | ||
|
|
d64e9b85a7 | ||
|
|
fb853edbe3 | ||
|
|
cc076c1be1 | ||
|
|
98cc9a6755 | ||
|
|
7bd2b9c23a | ||
|
|
de724a1ff3 | ||
|
|
2163055dae | ||
|
|
93ed0fc10b | ||
|
|
0d98cefd40 | ||
|
|
d58988a033 | ||
|
|
2acfab1e3f | ||
|
|
b915dfe9a6 | ||
|
|
25bd5a823e | ||
|
|
1c35de4716 | ||
|
|
4c00435a0a | ||
|
|
844e3079a8 | ||
|
|
4778cb5b2c | ||
|
|
ec5d60b919 | ||
|
|
e1f4b960e8 | ||
|
|
669e46da54 | ||
|
|
ba94cc5df7 | ||
|
|
d08245c3df | ||
|
|
5c18d12cbf | ||
|
|
580a42dec7 | ||
|
|
29286e159b | ||
|
|
19bcf90e9f | ||
|
|
dae9c00742 | ||
|
|
35324ceb7c | ||
|
|
5aadd47199 | ||
|
|
7d9057cc62 | ||
|
|
c4b322b883 | ||
|
|
19b09c898a | ||
|
|
eafe2098b6 | ||
|
|
2bc6a20d71 |
12
.eslintrc.json
Normal file
12
.eslintrc.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"es2021": true
|
||||||
|
},
|
||||||
|
"extends": "eslint:recommended",
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": 12
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
}
|
||||||
|
}
|
||||||
2
.gitattributes
vendored
2
.gitattributes
vendored
@@ -1,4 +1,6 @@
|
|||||||
* text eol=lf
|
* text eol=lf
|
||||||
|
|
||||||
|
*.reg text eol=crlf
|
||||||
|
|
||||||
*.png binary
|
*.png binary
|
||||||
*.gif binary
|
*.gif binary
|
||||||
|
|||||||
4
.vscode/launch.json
vendored
4
.vscode/launch.json
vendored
@@ -12,10 +12,12 @@
|
|||||||
//"-nw",
|
//"-nw",
|
||||||
"-ed",
|
"-ed",
|
||||||
"-emp",
|
"-emp",
|
||||||
|
"-e2d",
|
||||||
|
"-e2s",
|
||||||
"-a",
|
"-a",
|
||||||
"ed:wark",
|
"ed:wark",
|
||||||
"-v",
|
"-v",
|
||||||
"srv::r:aed"
|
"srv::r:aed:cnodupe"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
21
README.md
21
README.md
@@ -13,6 +13,17 @@ turn your phone or raspi into a portable file server with resumable uploads/down
|
|||||||
* code standard: `black`
|
* code standard: `black`
|
||||||
|
|
||||||
|
|
||||||
|
## quickstart
|
||||||
|
|
||||||
|
download [copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py) and you're all set!
|
||||||
|
|
||||||
|
running the sfx without arguments (for example doubleclicking it on Windows) will let anyone access the current folder; see `-h` for help if you want accounts and volumes etc
|
||||||
|
|
||||||
|
you may also want these, especially on servers:
|
||||||
|
* [contrib/systemd/copyparty.service](contrib/systemd/copyparty.service) to run copyparty as a systemd service
|
||||||
|
* [contrib/nginx/copyparty.conf](contrib/nginx/copyparty.conf) to reverse-proxy behind nginx (for legit https)
|
||||||
|
|
||||||
|
|
||||||
## notes
|
## notes
|
||||||
|
|
||||||
* iPhone/iPad: use Firefox to download files
|
* iPhone/iPad: use Firefox to download files
|
||||||
@@ -126,13 +137,15 @@ in the `scripts` folder:
|
|||||||
|
|
||||||
roughly sorted by priority
|
roughly sorted by priority
|
||||||
|
|
||||||
* up2k handle filename too long
|
* reduce up2k roundtrips
|
||||||
* up2k fails on empty files? alert then stuck
|
* start from a chunk index and just go
|
||||||
|
* terminate client on bad data
|
||||||
* drop onto folders
|
* drop onto folders
|
||||||
* look into android thumbnail cache file format
|
* `os.copy_file_range` for up2k cloning
|
||||||
|
* up2k partials ui
|
||||||
* support pillow-simd
|
* support pillow-simd
|
||||||
* cache sha512 chunks on client
|
* cache sha512 chunks on client
|
||||||
* symlink existing files on upload
|
|
||||||
* comment field
|
* comment field
|
||||||
|
* ~~look into android thumbnail cache file format~~ bad idea
|
||||||
* figure out the deal with pixel3a not being connectable as hotspot
|
* figure out the deal with pixel3a not being connectable as hotspot
|
||||||
* pixel3a having unpredictable 3sec latency in general :||||
|
* pixel3a having unpredictable 3sec latency in general :||||
|
||||||
|
|||||||
@@ -34,3 +34,8 @@ you could replace winfsp with [dokan](https://github.com/dokan-dev/dokany/releas
|
|||||||
* does the same thing except more correct, `samba` approves
|
* does the same thing except more correct, `samba` approves
|
||||||
* **supports Linux** -- expect `18 MiB/s` (wait what)
|
* **supports Linux** -- expect `18 MiB/s` (wait what)
|
||||||
* **supports Macos** -- probably
|
* **supports Macos** -- probably
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# copyparty-fuse-streaming.py
|
||||||
|
* pretend this doesn't exist
|
||||||
|
|||||||
1100
bin/copyparty-fuse-streaming.py
Executable file
1100
bin/copyparty-fuse-streaming.py
Executable file
File diff suppressed because it is too large
Load Diff
@@ -12,7 +12,7 @@ __url__ = "https://github.com/9001/copyparty/"
|
|||||||
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:3923/
|
python copyparty-fuse.py http://192.168.1.69:3923/ ./music
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
python3 -m pip install --user fusepy
|
python3 -m pip install --user fusepy
|
||||||
@@ -20,6 +20,10 @@ dependencies:
|
|||||||
+ on Macos: https://osxfuse.github.io/
|
+ on Macos: https://osxfuse.github.io/
|
||||||
+ on Windows: https://github.com/billziss-gh/winfsp/releases/latest
|
+ on Windows: https://github.com/billziss-gh/winfsp/releases/latest
|
||||||
|
|
||||||
|
note:
|
||||||
|
you probably want to run this on windows clients:
|
||||||
|
https://github.com/9001/copyparty/blob/master/contrib/explorer-nothumbs-nofoldertypes.reg
|
||||||
|
|
||||||
get server cert:
|
get server cert:
|
||||||
awk '/-BEGIN CERTIFICATE-/ {a=1} a; /-END CERTIFICATE-/{exit}' <(openssl s_client -connect 127.0.0.1:3923 </dev/null 2>/dev/null) >cert.pem
|
awk '/-BEGIN CERTIFICATE-/ {a=1} a; /-END CERTIFICATE-/{exit}' <(openssl s_client -connect 127.0.0.1:3923 </dev/null 2>/dev/null) >cert.pem
|
||||||
"""
|
"""
|
||||||
@@ -100,7 +104,7 @@ def rice_tid():
|
|||||||
|
|
||||||
|
|
||||||
def fancy_log(msg):
|
def fancy_log(msg):
|
||||||
print("{} {}\n".format(rice_tid(), msg), end="")
|
print("{:10.6f} {} {}\n".format(time.time() % 900, rice_tid(), msg), end="")
|
||||||
|
|
||||||
|
|
||||||
def null_log(msg):
|
def null_log(msg):
|
||||||
@@ -159,7 +163,7 @@ class RecentLog(object):
|
|||||||
thr.start()
|
thr.start()
|
||||||
|
|
||||||
def put(self, msg):
|
def put(self, msg):
|
||||||
msg = "{} {}\n".format(rice_tid(), msg)
|
msg = "{:10.6f} {} {}\n".format(time.time() % 900, rice_tid(), msg)
|
||||||
if self.f:
|
if self.f:
|
||||||
fmsg = " ".join([datetime.utcnow().strftime("%H%M%S.%f"), str(msg)])
|
fmsg = " ".join([datetime.utcnow().strftime("%H%M%S.%f"), str(msg)])
|
||||||
self.f.write(fmsg.encode("utf-8"))
|
self.f.write(fmsg.encode("utf-8"))
|
||||||
@@ -367,7 +371,7 @@ class Gateway(object):
|
|||||||
ret = []
|
ret = []
|
||||||
remainder = b""
|
remainder = b""
|
||||||
ptn = re.compile(
|
ptn = re.compile(
|
||||||
r'^<tr><td>(-|DIR)</td><td><a[^>]* href="([^"]+)"[^>]*>([^<]+)</a></td><td>([^<]+)</td><td>([^<]+)</td></tr>$'
|
r'^<tr><td>(-|DIR|<a [^<]+</a>)</td><td><a[^>]* href="([^"]+)"[^>]*>([^<]+)</a></td><td>([^<]+)</td><td>[^<]+</td><td>([^<]+)</td></tr>$'
|
||||||
)
|
)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
@@ -405,7 +409,7 @@ class Gateway(object):
|
|||||||
info("bad HTML or OS [{}] [{}]".format(fdate, fsize))
|
info("bad HTML or OS [{}] [{}]".format(fdate, fsize))
|
||||||
# python cannot strptime(1959-01-01) on windows
|
# python cannot strptime(1959-01-01) on windows
|
||||||
|
|
||||||
if ftype == "-":
|
if ftype != "DIR":
|
||||||
ret.append([fname, self.stat_file(ts, sz), 0])
|
ret.append([fname, self.stat_file(ts, sz), 0])
|
||||||
else:
|
else:
|
||||||
ret.append([fname, self.stat_dir(ts, sz), 0])
|
ret.append([fname, self.stat_dir(ts, sz), 0])
|
||||||
@@ -658,8 +662,18 @@ class CPPF(Operations):
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
if get2 - get1 <= 1024 * 1024:
|
if get2 - get1 <= 1024 * 1024:
|
||||||
h_ofs = get1 - 256 * 1024
|
# unless the request is for the last n bytes of the file,
|
||||||
|
# grow the start to cache some stuff around the range
|
||||||
|
if get2 < file_sz - 1:
|
||||||
|
h_ofs = get1 - 1024 * 256
|
||||||
|
else:
|
||||||
|
h_ofs = get1 - 1024 * 32
|
||||||
|
|
||||||
|
# likewise grow the end unless start is 0
|
||||||
|
if get1 > 0:
|
||||||
h_end = get2 + 1024 * 1024
|
h_end = get2 + 1024 * 1024
|
||||||
|
else:
|
||||||
|
h_end = get2 + 1024 * 64
|
||||||
else:
|
else:
|
||||||
# big enough, doesn't need pads
|
# big enough, doesn't need pads
|
||||||
h_ofs = get1
|
h_ofs = get1
|
||||||
@@ -705,6 +719,7 @@ class CPPF(Operations):
|
|||||||
self.dircache.append(cn)
|
self.dircache.append(cn)
|
||||||
self.clean_dircache()
|
self.clean_dircache()
|
||||||
|
|
||||||
|
# import pprint; pprint.pprint(ret)
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def readdir(self, path, fh=None):
|
def readdir(self, path, fh=None):
|
||||||
@@ -802,7 +817,11 @@ class CPPF(Operations):
|
|||||||
# dbg("=" + repr(cache_stat))
|
# dbg("=" + repr(cache_stat))
|
||||||
return cache_stat
|
return cache_stat
|
||||||
|
|
||||||
info("=ENOENT ({})".format(hexler(path)))
|
fun = info
|
||||||
|
if MACOS and path.split('/')[-1].startswith('._'):
|
||||||
|
fun = dbg
|
||||||
|
|
||||||
|
fun("=ENOENT ({})".format(hexler(path)))
|
||||||
raise FuseOSError(errno.ENOENT)
|
raise FuseOSError(errno.ENOENT)
|
||||||
|
|
||||||
access = None
|
access = None
|
||||||
@@ -906,6 +925,7 @@ class TheArgparseFormatter(
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
global info, log, dbg
|
global info, log, dbg
|
||||||
|
time.strptime("19970815", "%Y%m%d") # python#7980
|
||||||
|
|
||||||
# filecache helps for reads that are ~64k or smaller;
|
# filecache helps for reads that are ~64k or smaller;
|
||||||
# linux generally does 128k so the cache is a slowdown,
|
# linux generally does 128k so the cache is a slowdown,
|
||||||
|
|||||||
@@ -567,6 +567,8 @@ class CPPF(Fuse):
|
|||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
time.strptime("19970815", "%Y%m%d") # python#7980
|
||||||
|
|
||||||
server = CPPF()
|
server = CPPF()
|
||||||
server.parser.add_option(mountopt="url", metavar="BASE_URL", default=None)
|
server.parser.add_option(mountopt="url", metavar="BASE_URL", default=None)
|
||||||
server.parse(values=server, errex=1)
|
server.parse(values=server, errex=1)
|
||||||
|
|||||||
@@ -9,6 +9,9 @@
|
|||||||
* assumes the webserver and copyparty is running on the same server/IP
|
* 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
|
* modify `10.13.1.1` as necessary if you wish to support browsers without javascript
|
||||||
|
|
||||||
|
### [`explorer-nothumbs-nofoldertypes.reg`](explorer-nothumbs-nofoldertypes.reg)
|
||||||
|
disables thumbnails and folder-type detection in windows explorer, makes it way faster (especially for slow/networked locations (such as copyparty-fuse))
|
||||||
|
|
||||||
# OS integration
|
# OS integration
|
||||||
init-scripts to start copyparty as a service
|
init-scripts to start copyparty as a service
|
||||||
* [`systemd/copyparty.service`](systemd/copyparty.service)
|
* [`systemd/copyparty.service`](systemd/copyparty.service)
|
||||||
|
|||||||
31
contrib/explorer-nothumbs-nofoldertypes.reg
Normal file
31
contrib/explorer-nothumbs-nofoldertypes.reg
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
Windows Registry Editor Version 5.00
|
||||||
|
|
||||||
|
; this will do 3 things, all optional:
|
||||||
|
; 1) disable thumbnails
|
||||||
|
; 2) delete all existing folder type settings/detections
|
||||||
|
; 3) disable folder type detection (force default columns)
|
||||||
|
;
|
||||||
|
; this makes the file explorer way faster,
|
||||||
|
; especially on slow/networked locations
|
||||||
|
|
||||||
|
|
||||||
|
; =====================================================================
|
||||||
|
; 1) disable thumbnails
|
||||||
|
|
||||||
|
[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced]
|
||||||
|
"IconsOnly"=dword:00000001
|
||||||
|
|
||||||
|
|
||||||
|
; =====================================================================
|
||||||
|
; 2) delete all existing folder type settings/detections
|
||||||
|
|
||||||
|
[-HKEY_CURRENT_USER\Software\Classes\Local Settings\Software\Microsoft\Windows\Shell\Bags]
|
||||||
|
|
||||||
|
[-HKEY_CURRENT_USER\Software\Classes\Local Settings\Software\Microsoft\Windows\Shell\BagMRU]
|
||||||
|
|
||||||
|
|
||||||
|
; =====================================================================
|
||||||
|
; 3) disable folder type detection
|
||||||
|
|
||||||
|
[HKEY_CURRENT_USER\Software\Classes\Local Settings\Software\Microsoft\Windows\Shell\Bags\AllFolders\Shell]
|
||||||
|
"FolderType"="NotSpecified"
|
||||||
@@ -9,6 +9,7 @@ __license__ = "MIT"
|
|||||||
__url__ = "https://github.com/9001/copyparty/"
|
__url__ = "https://github.com/9001/copyparty/"
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import time
|
||||||
import shutil
|
import shutil
|
||||||
import filecmp
|
import filecmp
|
||||||
import locale
|
import locale
|
||||||
@@ -85,6 +86,7 @@ def ensure_cert():
|
|||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
time.strptime("19970815", "%Y%m%d") # python#7980
|
||||||
if WINDOWS:
|
if WINDOWS:
|
||||||
os.system("") # enables colors
|
os.system("") # enables colors
|
||||||
|
|
||||||
@@ -103,17 +105,22 @@ def main():
|
|||||||
epilog=dedent(
|
epilog=dedent(
|
||||||
"""
|
"""
|
||||||
-a takes username:password,
|
-a takes username:password,
|
||||||
-v takes src:dst:permset:permset:... where "permset" is
|
-v takes src:dst:permset:permset:cflag:cflag:...
|
||||||
accesslevel followed by username (no separator)
|
where "permset" is accesslevel followed by username (no separator)
|
||||||
|
and "cflag" is config flags to set on this volume
|
||||||
|
|
||||||
|
list of cflags:
|
||||||
|
cnodupe rejects existing files (instead of symlinking them)
|
||||||
|
|
||||||
example:\033[35m
|
example:\033[35m
|
||||||
-a ed:hunter2 -v .::r:aed -v ../inc:dump:w:aed \033[36m
|
-a ed:hunter2 -v .::r:aed -v ../inc:dump:w:aed:cnodupe \033[36m
|
||||||
mount current directory at "/" with
|
mount current directory at "/" with
|
||||||
* r (read-only) for everyone
|
* r (read-only) for everyone
|
||||||
* a (read+write) for ed
|
* a (read+write) for ed
|
||||||
mount ../inc at "/dump" with
|
mount ../inc at "/dump" with
|
||||||
* w (write-only) for everyone
|
* w (write-only) for everyone
|
||||||
* a (read+write) for ed \033[0m
|
* a (read+write) for ed
|
||||||
|
* reject duplicate files \033[0m
|
||||||
|
|
||||||
if no accounts or volumes are configured,
|
if no accounts or volumes are configured,
|
||||||
current folder will be read/write for everyone
|
current folder will be read/write for everyone
|
||||||
@@ -123,24 +130,26 @@ def main():
|
|||||||
"""
|
"""
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
ap.add_argument(
|
# fmt: off
|
||||||
"-c", metavar="PATH", type=str, action="append", help="add config file"
|
ap.add_argument("-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=3923, 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=64, help="max num clients")
|
ap.add_argument("-nc", metavar="NUM", type=int, default=64, help="max num clients")
|
||||||
ap.add_argument(
|
ap.add_argument("-j", metavar="CORES", type=int, default=1, help="max num cpu cores")
|
||||||
"-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("-ed", action="store_true", help="enable ?dots")
|
ap.add_argument("-ed", action="store_true", help="enable ?dots")
|
||||||
ap.add_argument("-emp", action="store_true", help="enable markdown plugins")
|
ap.add_argument("-emp", action="store_true", help="enable markdown plugins")
|
||||||
|
ap.add_argument("-e2d", action="store_true", help="enable up2k database")
|
||||||
|
ap.add_argument("-e2s", action="store_true", help="enable up2k db-scanner")
|
||||||
|
ap.add_argument("-mcr", metavar="SEC", type=int, default=60, help="md-editor mod-chk rate")
|
||||||
ap.add_argument("-nw", action="store_true", help="disable writes (benchmark)")
|
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("-nih", action="store_true", help="no info hostname")
|
||||||
ap.add_argument("-nid", action="store_true", help="no info disk-usage")
|
ap.add_argument("-nid", action="store_true", help="no info disk-usage")
|
||||||
|
ap.add_argument("--no-sendfile", action="store_true", help="disable sendfile")
|
||||||
al = ap.parse_args()
|
al = ap.parse_args()
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
SvcHub(al).run()
|
SvcHub(al).run()
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
|
|
||||||
VERSION = (0, 5, 6)
|
VERSION = (0, 7, 1)
|
||||||
CODENAME = "fuse jelly"
|
CODENAME = "keeping track"
|
||||||
BUILD_DT = (2020, 11, 29)
|
BUILD_DT = (2021, 1, 23)
|
||||||
|
|
||||||
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)
|
||||||
|
|||||||
@@ -12,11 +12,12 @@ from .util import undot, Pebkac, fsdec, fsenc
|
|||||||
class VFS(object):
|
class VFS(object):
|
||||||
"""single level in the virtual fs"""
|
"""single level in the virtual fs"""
|
||||||
|
|
||||||
def __init__(self, realpath, vpath, uread=[], uwrite=[]):
|
def __init__(self, realpath, vpath, uread=[], uwrite=[], flags={}):
|
||||||
self.realpath = realpath # absolute path on host filesystem
|
self.realpath = realpath # absolute path on host filesystem
|
||||||
self.vpath = vpath # absolute path in the virtual filesystem
|
self.vpath = vpath # absolute path in the virtual filesystem
|
||||||
self.uread = uread # users who can read this
|
self.uread = uread # users who can read this
|
||||||
self.uwrite = uwrite # users who can write this
|
self.uwrite = uwrite # users who can write this
|
||||||
|
self.flags = flags # config switches
|
||||||
self.nodes = {} # child nodes
|
self.nodes = {} # child nodes
|
||||||
|
|
||||||
def add(self, src, dst):
|
def add(self, src, dst):
|
||||||
@@ -36,6 +37,7 @@ class VFS(object):
|
|||||||
"{}/{}".format(self.vpath, name).lstrip("/"),
|
"{}/{}".format(self.vpath, name).lstrip("/"),
|
||||||
self.uread,
|
self.uread,
|
||||||
self.uwrite,
|
self.uwrite,
|
||||||
|
self.flags,
|
||||||
)
|
)
|
||||||
self.nodes[name] = vn
|
self.nodes[name] = vn
|
||||||
return vn.add(src, dst)
|
return vn.add(src, dst)
|
||||||
@@ -104,7 +106,7 @@ class VFS(object):
|
|||||||
real.sort()
|
real.sort()
|
||||||
if not rem:
|
if not rem:
|
||||||
for name, vn2 in sorted(self.nodes.items()):
|
for name, vn2 in sorted(self.nodes.items()):
|
||||||
if uname in vn2.uread:
|
if uname in vn2.uread or "*" in vn2.uread:
|
||||||
virt_vis[name] = vn2
|
virt_vis[name] = vn2
|
||||||
|
|
||||||
# no vfs nodes in the list of real inodes
|
# no vfs nodes in the list of real inodes
|
||||||
@@ -161,7 +163,7 @@ class AuthSrv(object):
|
|||||||
|
|
||||||
yield prev, True
|
yield prev, True
|
||||||
|
|
||||||
def _parse_config_file(self, fd, user, mread, mwrite, mount):
|
def _parse_config_file(self, fd, user, mread, mwrite, mflags, mount):
|
||||||
vol_src = None
|
vol_src = None
|
||||||
vol_dst = None
|
vol_dst = None
|
||||||
for ln in [x.decode("utf-8").strip() for x in fd]:
|
for ln in [x.decode("utf-8").strip() for x in fd]:
|
||||||
@@ -191,6 +193,7 @@ class AuthSrv(object):
|
|||||||
mount[vol_dst] = vol_src
|
mount[vol_dst] = vol_src
|
||||||
mread[vol_dst] = []
|
mread[vol_dst] = []
|
||||||
mwrite[vol_dst] = []
|
mwrite[vol_dst] = []
|
||||||
|
mflags[vol_dst] = {}
|
||||||
continue
|
continue
|
||||||
|
|
||||||
lvl, uname = ln.split(" ")
|
lvl, uname = ln.split(" ")
|
||||||
@@ -198,6 +201,9 @@ class AuthSrv(object):
|
|||||||
mread[vol_dst].append(uname)
|
mread[vol_dst].append(uname)
|
||||||
if lvl in "wa":
|
if lvl in "wa":
|
||||||
mwrite[vol_dst].append(uname)
|
mwrite[vol_dst].append(uname)
|
||||||
|
if lvl == "c":
|
||||||
|
# config option, currently switches only
|
||||||
|
mflags[vol_dst][uname] = True
|
||||||
|
|
||||||
def reload(self):
|
def reload(self):
|
||||||
"""
|
"""
|
||||||
@@ -210,6 +216,7 @@ class AuthSrv(object):
|
|||||||
user = {} # username:password
|
user = {} # username:password
|
||||||
mread = {} # mountpoint:[username]
|
mread = {} # mountpoint:[username]
|
||||||
mwrite = {} # mountpoint:[username]
|
mwrite = {} # mountpoint:[username]
|
||||||
|
mflags = {} # mountpoint:[flag]
|
||||||
mount = {} # dst:src (mountpoint:realpath)
|
mount = {} # dst:src (mountpoint:realpath)
|
||||||
|
|
||||||
if self.args.a:
|
if self.args.a:
|
||||||
@@ -232,9 +239,13 @@ class AuthSrv(object):
|
|||||||
mount[dst] = src
|
mount[dst] = src
|
||||||
mread[dst] = []
|
mread[dst] = []
|
||||||
mwrite[dst] = []
|
mwrite[dst] = []
|
||||||
|
mflags[dst] = {}
|
||||||
|
|
||||||
perms = perms.split(":")
|
perms = perms.split(":")
|
||||||
for (lvl, uname) in [[x[0], x[1:]] for x in perms]:
|
for (lvl, uname) in [[x[0], x[1:]] for x in perms]:
|
||||||
|
if lvl == "c":
|
||||||
|
# config option, currently switches only
|
||||||
|
mflags[dst][uname] = True
|
||||||
if uname == "":
|
if uname == "":
|
||||||
uname = "*"
|
uname = "*"
|
||||||
if lvl in "ra":
|
if lvl in "ra":
|
||||||
@@ -245,14 +256,15 @@ class AuthSrv(object):
|
|||||||
if self.args.c:
|
if self.args.c:
|
||||||
for cfg_fn in self.args.c:
|
for cfg_fn in self.args.c:
|
||||||
with open(cfg_fn, "rb") as f:
|
with open(cfg_fn, "rb") as f:
|
||||||
self._parse_config_file(f, user, mread, mwrite, mount)
|
self._parse_config_file(f, user, mread, mwrite, mflags, mount)
|
||||||
|
|
||||||
|
self.all_writable = []
|
||||||
if not mount:
|
if not mount:
|
||||||
# -h says our defaults are CWD at root and read/write for everyone
|
# -h says our defaults are CWD at root and read/write for everyone
|
||||||
vfs = VFS(os.path.abspath("."), "", ["*"], ["*"])
|
vfs = VFS(os.path.abspath("."), "", ["*"], ["*"])
|
||||||
elif "" not in mount:
|
elif "" not in mount:
|
||||||
# there's volumes but no root; make root inaccessible
|
# there's volumes but no root; make root inaccessible
|
||||||
vfs = VFS(os.path.abspath("."), "", [], [])
|
vfs = VFS(os.path.abspath("."), "")
|
||||||
|
|
||||||
maxdepth = 0
|
maxdepth = 0
|
||||||
for dst in sorted(mount.keys(), key=lambda x: (x.count("/"), len(x))):
|
for dst in sorted(mount.keys(), key=lambda x: (x.count("/"), len(x))):
|
||||||
@@ -262,12 +274,18 @@ class AuthSrv(object):
|
|||||||
|
|
||||||
if dst == "":
|
if dst == "":
|
||||||
# rootfs was mapped; fully replaces the default CWD vfs
|
# rootfs was mapped; fully replaces the default CWD vfs
|
||||||
vfs = VFS(mount[dst], dst, mread[dst], mwrite[dst])
|
vfs = VFS(mount[dst], dst, mread[dst], mwrite[dst], mflags[dst])
|
||||||
continue
|
continue
|
||||||
|
|
||||||
v = vfs.add(mount[dst], dst)
|
v = vfs.add(mount[dst], dst)
|
||||||
v.uread = mread[dst]
|
v.uread = mread[dst]
|
||||||
v.uwrite = mwrite[dst]
|
v.uwrite = mwrite[dst]
|
||||||
|
v.flags = mflags[dst]
|
||||||
|
if v.uwrite:
|
||||||
|
self.all_writable.append(v)
|
||||||
|
|
||||||
|
if vfs.uwrite and vfs not in self.all_writable:
|
||||||
|
self.all_writable.append(vfs)
|
||||||
|
|
||||||
missing_users = {}
|
missing_users = {}
|
||||||
for d in [mread, mwrite]:
|
for d in [mread, mwrite]:
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ class HttpCli(object):
|
|||||||
self.conn = conn
|
self.conn = conn
|
||||||
self.s = conn.s
|
self.s = conn.s
|
||||||
self.sr = conn.sr
|
self.sr = conn.sr
|
||||||
|
self.ip = conn.addr[0]
|
||||||
self.addr = conn.addr
|
self.addr = conn.addr
|
||||||
self.args = conn.args
|
self.args = conn.args
|
||||||
self.auth = conn.auth
|
self.auth = conn.auth
|
||||||
@@ -42,7 +43,7 @@ class HttpCli(object):
|
|||||||
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 [404]
|
return ex.code < 400 or ex.code == 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
|
||||||
@@ -85,7 +86,8 @@ class HttpCli(object):
|
|||||||
|
|
||||||
v = self.headers.get("x-forwarded-for", None)
|
v = self.headers.get("x-forwarded-for", None)
|
||||||
if v is not None and self.conn.addr[0] in ["127.0.0.1", "::1"]:
|
if v is not None and self.conn.addr[0] in ["127.0.0.1", "::1"]:
|
||||||
self.log_src = self.conn.set_rproxy(v.split(",")[0])
|
self.ip = v.split(",")[0]
|
||||||
|
self.log_src = self.conn.set_rproxy(self.ip)
|
||||||
|
|
||||||
self.uname = "*"
|
self.uname = "*"
|
||||||
if "cookie" in self.headers:
|
if "cookie" in self.headers:
|
||||||
@@ -305,7 +307,7 @@ class HttpCli(object):
|
|||||||
vfs, rem = self.conn.auth.vfs.get(self.vpath, self.uname, False, True)
|
vfs, rem = self.conn.auth.vfs.get(self.vpath, self.uname, False, True)
|
||||||
fdir = os.path.join(vfs.realpath, rem)
|
fdir = os.path.join(vfs.realpath, rem)
|
||||||
|
|
||||||
addr = self.conn.addr[0].replace(":", ".")
|
addr = self.ip.replace(":", ".")
|
||||||
fn = "put-{:.6f}-{}.bin".format(time.time(), addr)
|
fn = "put-{:.6f}-{}.bin".format(time.time(), addr)
|
||||||
path = os.path.join(fdir, fn)
|
path = os.path.join(fdir, fn)
|
||||||
|
|
||||||
@@ -384,9 +386,11 @@ class HttpCli(object):
|
|||||||
|
|
||||||
vfs, rem = self.conn.auth.vfs.get(self.vpath, self.uname, False, True)
|
vfs, rem = self.conn.auth.vfs.get(self.vpath, self.uname, False, True)
|
||||||
|
|
||||||
body["vdir"] = self.vpath
|
body["vtop"] = vfs.vpath
|
||||||
body["rdir"] = os.path.join(vfs.realpath, rem)
|
body["ptop"] = vfs.realpath
|
||||||
body["addr"] = self.addr[0]
|
body["prel"] = rem
|
||||||
|
body["addr"] = self.ip
|
||||||
|
body["flag"] = vfs.flags
|
||||||
|
|
||||||
x = self.conn.hsrv.broker.put(True, "up2k.handle_json", body)
|
x = self.conn.hsrv.broker.put(True, "up2k.handle_json", body)
|
||||||
response = x.get()
|
response = x.get()
|
||||||
@@ -408,7 +412,10 @@ class HttpCli(object):
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
raise Pebkac(400, "need hash and wark headers for binary POST")
|
raise Pebkac(400, "need hash and wark headers for binary POST")
|
||||||
|
|
||||||
x = self.conn.hsrv.broker.put(True, "up2k.handle_chunk", wark, chash)
|
vfs, _ = self.conn.auth.vfs.get(self.vpath, self.uname, False, True)
|
||||||
|
ptop = vfs.realpath
|
||||||
|
|
||||||
|
x = self.conn.hsrv.broker.put(True, "up2k.handle_chunk", ptop, wark, chash)
|
||||||
response = x.get()
|
response = x.get()
|
||||||
chunksize, cstart, path, lastmod = response
|
chunksize, cstart, path, lastmod = response
|
||||||
|
|
||||||
@@ -453,8 +460,8 @@ class HttpCli(object):
|
|||||||
|
|
||||||
self.log("clone {} done".format(cstart[0]))
|
self.log("clone {} done".format(cstart[0]))
|
||||||
|
|
||||||
x = self.conn.hsrv.broker.put(True, "up2k.confirm_chunk", wark, chash)
|
x = self.conn.hsrv.broker.put(True, "up2k.confirm_chunk", ptop, wark, chash)
|
||||||
num_left = x.get()
|
num_left, path = x.get()
|
||||||
|
|
||||||
if not WINDOWS and num_left == 0:
|
if not WINDOWS and num_left == 0:
|
||||||
times = (int(time.time()), int(lastmod))
|
times = (int(time.time()), int(lastmod))
|
||||||
@@ -568,24 +575,24 @@ class HttpCli(object):
|
|||||||
self.log("discarding incoming file without filename")
|
self.log("discarding incoming file without filename")
|
||||||
# fallthrough
|
# fallthrough
|
||||||
|
|
||||||
fn = os.devnull
|
|
||||||
if p_file and not nullwrite:
|
if p_file and not nullwrite:
|
||||||
fdir = os.path.join(vfs.realpath, rem)
|
fdir = os.path.join(vfs.realpath, rem)
|
||||||
fn = os.path.join(fdir, sanitize_fn(p_file))
|
fname = sanitize_fn(p_file)
|
||||||
|
|
||||||
if not os.path.isdir(fsenc(fdir)):
|
if not os.path.isdir(fsenc(fdir)):
|
||||||
raise Pebkac(404, "that folder does not exist")
|
raise Pebkac(404, "that folder does not exist")
|
||||||
|
|
||||||
# TODO broker which avoid this race and
|
suffix = ".{:.6f}-{}".format(time.time(), self.ip)
|
||||||
# provides a new filename if taken (same as up2k)
|
open_args = {"fdir": fdir, "suffix": suffix}
|
||||||
if os.path.exists(fsenc(fn)):
|
else:
|
||||||
fn += ".{:.6f}-{}".format(time.time(), self.addr[0])
|
open_args = {}
|
||||||
# using current-time instead of t0 cause clients
|
fname = os.devnull
|
||||||
# may reuse a name for multiple files in one post
|
fdir = ""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(fsenc(fn), "wb") as f:
|
with ren_open(fname, "wb", 512 * 1024, **open_args) as f:
|
||||||
self.log("writing to {0}".format(fn))
|
f, fname = f["orz"]
|
||||||
|
self.log("writing to {}/{}".format(fdir, fname))
|
||||||
sz, sha512_hex, _ = hashcopy(self.conn, p_data, f)
|
sz, sha512_hex, _ = hashcopy(self.conn, p_data, f)
|
||||||
if sz == 0:
|
if sz == 0:
|
||||||
raise Pebkac(400, "empty files in post")
|
raise Pebkac(400, "empty files in post")
|
||||||
@@ -594,8 +601,14 @@ class HttpCli(object):
|
|||||||
self.conn.nbyte += sz
|
self.conn.nbyte += sz
|
||||||
|
|
||||||
except Pebkac:
|
except Pebkac:
|
||||||
if fn != os.devnull:
|
if fname != os.devnull:
|
||||||
os.rename(fsenc(fn), fsenc(fn + ".PARTIAL"))
|
fp = os.path.join(fdir, fname)
|
||||||
|
suffix = ".PARTIAL"
|
||||||
|
try:
|
||||||
|
os.rename(fsenc(fp), fsenc(fp + suffix))
|
||||||
|
except:
|
||||||
|
fp = fp[: -len(suffix)]
|
||||||
|
os.rename(fsenc(fp), fsenc(fp + suffix))
|
||||||
|
|
||||||
raise
|
raise
|
||||||
|
|
||||||
@@ -631,7 +644,7 @@ class HttpCli(object):
|
|||||||
"\n".join(
|
"\n".join(
|
||||||
unicode(x)
|
unicode(x)
|
||||||
for x in [
|
for x in [
|
||||||
":".join(unicode(x) for x in self.addr),
|
":".join(unicode(x) for x in [self.ip, self.addr[1]]),
|
||||||
msg.rstrip(),
|
msg.rstrip(),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@@ -680,7 +693,7 @@ class HttpCli(object):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
fp = os.path.join(vfs.realpath, rem)
|
fp = os.path.join(vfs.realpath, rem)
|
||||||
srv_lastmod = -1
|
srv_lastmod = srv_lastmod3 = -1
|
||||||
try:
|
try:
|
||||||
st = os.stat(fsenc(fp))
|
st = os.stat(fsenc(fp))
|
||||||
srv_lastmod = st.st_mtime
|
srv_lastmod = st.st_mtime
|
||||||
@@ -731,7 +744,7 @@ class HttpCli(object):
|
|||||||
if p_field != "body":
|
if p_field != "body":
|
||||||
raise Pebkac(400, "expected body, got {}".format(p_field))
|
raise Pebkac(400, "expected body, got {}".format(p_field))
|
||||||
|
|
||||||
with open(fp, "wb") as f:
|
with open(fp, "wb", 512 * 1024) as f:
|
||||||
sz, sha512, _ = hashcopy(self.conn, p_data, f)
|
sz, sha512, _ = hashcopy(self.conn, p_data, f)
|
||||||
|
|
||||||
new_lastmod = os.stat(fsenc(fp)).st_mtime
|
new_lastmod = os.stat(fsenc(fp)).st_mtime
|
||||||
@@ -756,9 +769,12 @@ class HttpCli(object):
|
|||||||
cli_dt = time.strptime(cli_lastmod, "%a, %d %b %Y %H:%M:%S GMT")
|
cli_dt = time.strptime(cli_lastmod, "%a, %d %b %Y %H:%M:%S GMT")
|
||||||
cli_ts = calendar.timegm(cli_dt)
|
cli_ts = calendar.timegm(cli_dt)
|
||||||
return file_lastmod, int(file_ts) > int(cli_ts)
|
return file_lastmod, int(file_ts) > int(cli_ts)
|
||||||
except:
|
except Exception as ex:
|
||||||
self.log("bad lastmod format: {}".format(cli_lastmod))
|
self.log(
|
||||||
self.log(" expected format: {}".format(file_lastmod))
|
"lastmod {}\nremote: [{}]\n local: [{}]".format(
|
||||||
|
repr(ex), cli_lastmod, file_lastmod
|
||||||
|
)
|
||||||
|
)
|
||||||
return file_lastmod, file_lastmod != cli_lastmod
|
return file_lastmod, file_lastmod != cli_lastmod
|
||||||
|
|
||||||
return file_lastmod, True
|
return file_lastmod, True
|
||||||
@@ -875,6 +891,7 @@ class HttpCli(object):
|
|||||||
|
|
||||||
logtail += " [\033[36m{}-{}\033[0m]".format(lower, upper)
|
logtail += " [\033[36m{}-{}\033[0m]".format(lower, upper)
|
||||||
|
|
||||||
|
use_sendfile = False
|
||||||
if decompress:
|
if decompress:
|
||||||
open_func = gzip.open
|
open_func = gzip.open
|
||||||
open_args = [fsenc(fs_path), "rb"]
|
open_args = [fsenc(fs_path), "rb"]
|
||||||
@@ -884,6 +901,8 @@ class HttpCli(object):
|
|||||||
open_func = open
|
open_func = open
|
||||||
# 512 kB is optimal for huge files, use 64k
|
# 512 kB is optimal for huge files, use 64k
|
||||||
open_args = [fsenc(fs_path), "rb", 64 * 1024]
|
open_args = [fsenc(fs_path), "rb", 64 * 1024]
|
||||||
|
if hasattr(os, "sendfile"):
|
||||||
|
use_sendfile = not self.args.no_sendfile
|
||||||
|
|
||||||
#
|
#
|
||||||
# send reply
|
# send reply
|
||||||
@@ -906,24 +925,13 @@ class HttpCli(object):
|
|||||||
|
|
||||||
ret = True
|
ret = True
|
||||||
with open_func(*open_args) as f:
|
with open_func(*open_args) as f:
|
||||||
remains = upper - lower
|
if use_sendfile:
|
||||||
f.seek(lower)
|
remains = sendfile_kern(lower, upper, f, self.s)
|
||||||
while remains > 0:
|
else:
|
||||||
# time.sleep(0.01)
|
remains = sendfile_py(lower, upper, f, self.s)
|
||||||
buf = f.read(4096)
|
|
||||||
if not buf:
|
|
||||||
break
|
|
||||||
|
|
||||||
if remains < len(buf):
|
if remains > 0:
|
||||||
buf = buf[:remains]
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.s.sendall(buf)
|
|
||||||
remains -= len(buf)
|
|
||||||
except:
|
|
||||||
logmsg += " \033[31m" + str(upper - remains) + "\033[0m"
|
logmsg += " \033[31m" + str(upper - remains) + "\033[0m"
|
||||||
ret = False
|
|
||||||
break
|
|
||||||
|
|
||||||
spd = self._spd((upper - lower) - remains)
|
spd = self._spd((upper - lower) - remains)
|
||||||
self.log("{}, {}".format(logmsg, spd))
|
self.log("{}, {}".format(logmsg, spd))
|
||||||
@@ -964,6 +972,7 @@ class HttpCli(object):
|
|||||||
"title": html_escape(self.vpath),
|
"title": html_escape(self.vpath),
|
||||||
"lastmod": int(ts_md * 1000),
|
"lastmod": int(ts_md * 1000),
|
||||||
"md_plug": "true" if self.args.emp else "false",
|
"md_plug": "true" if self.args.emp else "false",
|
||||||
|
"md_chk_rate": self.args.mcr,
|
||||||
"md": "",
|
"md": "",
|
||||||
}
|
}
|
||||||
sz_html = len(template.render(**targs).encode("utf-8"))
|
sz_html = len(template.render(**targs).encode("utf-8"))
|
||||||
@@ -1018,6 +1027,10 @@ class HttpCli(object):
|
|||||||
if abspath.endswith(".md") and "raw" not in self.uparam:
|
if abspath.endswith(".md") and "raw" not in self.uparam:
|
||||||
return self.tx_md(abspath)
|
return self.tx_md(abspath)
|
||||||
|
|
||||||
|
bad = "{0}.hist{0}up2k.".format(os.sep)
|
||||||
|
if abspath.endswith(bad + "db") or abspath.endswith(bad + "snap"):
|
||||||
|
raise Pebkac(403)
|
||||||
|
|
||||||
return self.tx_file(abspath)
|
return self.tx_file(abspath)
|
||||||
|
|
||||||
fsroot, vfs_ls, vfs_virt = vn.ls(rem, self.uname)
|
fsroot, vfs_ls, vfs_virt = vn.ls(rem, self.uname)
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ class HttpConn(object):
|
|||||||
color = 34
|
color = 34
|
||||||
self.rproxy = ip
|
self.rproxy = ip
|
||||||
|
|
||||||
|
self.ip = ip
|
||||||
self.log_src = "{} \033[{}m{}".format(ip, color, self.addr[1]).ljust(26)
|
self.log_src = "{} \033[{}m{}".format(ip, color, self.addr[1]).ljust(26)
|
||||||
return self.log_src
|
return self.log_src
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from datetime import datetime, timedelta
|
|||||||
import calendar
|
import calendar
|
||||||
|
|
||||||
from .__init__ import PY2, WINDOWS, MACOS, VT100
|
from .__init__ import PY2, WINDOWS, MACOS, VT100
|
||||||
|
from .authsrv import AuthSrv
|
||||||
from .tcpsrv import TcpSrv
|
from .tcpsrv import TcpSrv
|
||||||
from .up2k import Up2k
|
from .up2k import Up2k
|
||||||
from .util import mp
|
from .util import mp
|
||||||
@@ -38,6 +39,10 @@ class SvcHub(object):
|
|||||||
self.tcpsrv = TcpSrv(self)
|
self.tcpsrv = TcpSrv(self)
|
||||||
self.up2k = Up2k(self)
|
self.up2k = Up2k(self)
|
||||||
|
|
||||||
|
if self.args.e2d and self.args.e2s:
|
||||||
|
auth = AuthSrv(self.args, self.log)
|
||||||
|
self.up2k.build_indexes(auth.all_writable)
|
||||||
|
|
||||||
# decide which worker impl to use
|
# decide which worker impl to use
|
||||||
if self.check_mp_enable():
|
if self.check_mp_enable():
|
||||||
from .broker_mp import BrokerMp as Broker
|
from .broker_mp import BrokerMp as Broker
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ class TcpSrv(object):
|
|||||||
|
|
||||||
self.srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
self.srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
self.srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
self.srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
self.srv.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||||||
try:
|
try:
|
||||||
self.srv.bind((self.args.i, self.args.p))
|
self.srv.bind((self.args.i, self.args.p))
|
||||||
except (OSError, socket.error) as ex:
|
except (OSError, socket.error) as ex:
|
||||||
|
|||||||
@@ -6,14 +6,25 @@ import os
|
|||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
import math
|
import math
|
||||||
|
import json
|
||||||
|
import gzip
|
||||||
|
import stat
|
||||||
import shutil
|
import shutil
|
||||||
import base64
|
import base64
|
||||||
import hashlib
|
import hashlib
|
||||||
import threading
|
import threading
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
|
||||||
from .__init__ import WINDOWS
|
from .__init__ import WINDOWS, PY2
|
||||||
from .util import Pebkac, Queue, fsenc, sanitize_fn
|
from .util import Pebkac, Queue, fsdec, fsenc, sanitize_fn, ren_open, atomic_move
|
||||||
|
|
||||||
|
HAVE_SQLITE3 = False
|
||||||
|
try:
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
HAVE_SQLITE3 = True
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Up2k(object):
|
class Up2k(object):
|
||||||
@@ -22,20 +33,21 @@ class Up2k(object):
|
|||||||
* documentation
|
* documentation
|
||||||
* registry persistence
|
* registry persistence
|
||||||
* ~/.config flatfiles for active jobs
|
* ~/.config flatfiles for active jobs
|
||||||
* wark->path database for finished uploads
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, broker):
|
def __init__(self, broker):
|
||||||
self.broker = broker
|
self.broker = broker
|
||||||
self.args = broker.args
|
self.args = broker.args
|
||||||
self.log = broker.log
|
self.log = broker.log
|
||||||
|
self.persist = self.args.e2d
|
||||||
|
|
||||||
# config
|
# config
|
||||||
self.salt = "hunter2" # TODO: config
|
self.salt = "hunter2" # TODO: config
|
||||||
|
|
||||||
# state
|
# state
|
||||||
self.registry = {}
|
|
||||||
self.mutex = threading.Lock()
|
self.mutex = threading.Lock()
|
||||||
|
self.registry = {}
|
||||||
|
self.db = {}
|
||||||
|
|
||||||
if WINDOWS:
|
if WINDOWS:
|
||||||
# usually fails to set lastmod too quickly
|
# usually fails to set lastmod too quickly
|
||||||
@@ -44,54 +56,301 @@ class Up2k(object):
|
|||||||
thr.daemon = True
|
thr.daemon = True
|
||||||
thr.start()
|
thr.start()
|
||||||
|
|
||||||
|
if self.persist:
|
||||||
|
thr = threading.Thread(target=self._snapshot)
|
||||||
|
thr.daemon = True
|
||||||
|
thr.start()
|
||||||
|
|
||||||
# static
|
# static
|
||||||
self.r_hash = re.compile("^[0-9a-zA-Z_-]{43}$")
|
self.r_hash = re.compile("^[0-9a-zA-Z_-]{43}$")
|
||||||
|
|
||||||
|
if self.persist and not HAVE_SQLITE3:
|
||||||
|
m = "could not initialize sqlite3, will use in-memory registry only"
|
||||||
|
self.log("up2k", m)
|
||||||
|
|
||||||
|
def _vis_job_progress(self, job):
|
||||||
|
perc = 100 - (len(job["need"]) * 100.0 / len(job["hash"]))
|
||||||
|
path = os.path.join(job["ptop"], job["prel"], job["name"])
|
||||||
|
return "{:5.1f}% {}".format(perc, path)
|
||||||
|
|
||||||
|
def _vis_reg_progress(self, reg):
|
||||||
|
ret = []
|
||||||
|
for _, job in reg.items():
|
||||||
|
ret.append(self._vis_job_progress(job))
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def register_vpath(self, ptop):
|
||||||
|
with self.mutex:
|
||||||
|
if ptop in self.registry:
|
||||||
|
return None
|
||||||
|
|
||||||
|
reg = {}
|
||||||
|
path = os.path.join(ptop, ".hist", "up2k.snap")
|
||||||
|
if self.persist and os.path.exists(path):
|
||||||
|
with gzip.GzipFile(path, "rb") as f:
|
||||||
|
j = f.read().decode("utf-8")
|
||||||
|
|
||||||
|
reg = json.loads(j)
|
||||||
|
for _, job in reg.items():
|
||||||
|
job["poke"] = time.time()
|
||||||
|
|
||||||
|
m = "loaded snap {} |{}|".format(path, len(reg.keys()))
|
||||||
|
m = [m] + self._vis_reg_progress(reg)
|
||||||
|
self.log("up2k", "\n".join(m))
|
||||||
|
|
||||||
|
self.registry[ptop] = reg
|
||||||
|
if not self.persist or not HAVE_SQLITE3:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.mkdir(os.path.join(ptop, ".hist"))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
db_path = os.path.join(ptop, ".hist", "up2k.db")
|
||||||
|
if ptop in self.db:
|
||||||
|
# self.db[ptop].close()
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
db = self._open_db(db_path)
|
||||||
|
self.db[ptop] = db
|
||||||
|
return db
|
||||||
|
except Exception as ex:
|
||||||
|
m = "failed to open [{}]: {}".format(ptop, repr(ex))
|
||||||
|
self.log("up2k", m)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def build_indexes(self, writeables):
|
||||||
|
tops = [d.realpath for d in writeables]
|
||||||
|
for top in tops:
|
||||||
|
db = self.register_vpath(top)
|
||||||
|
if db:
|
||||||
|
# can be symlink so don't `and d.startswith(top)``
|
||||||
|
excl = set([d for d in tops if d != top])
|
||||||
|
self._build_dir([db, 0], top, excl, top)
|
||||||
|
self._drop_lost(db, top)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
def _build_dir(self, dbw, top, excl, cdir):
|
||||||
|
try:
|
||||||
|
inodes = [fsdec(x) for x in os.listdir(fsenc(cdir))]
|
||||||
|
except Exception as ex:
|
||||||
|
self.log("up2k", "listdir: " + repr(ex))
|
||||||
|
return
|
||||||
|
|
||||||
|
histdir = os.path.join(top, ".hist")
|
||||||
|
for inode in inodes:
|
||||||
|
abspath = os.path.join(cdir, inode)
|
||||||
|
try:
|
||||||
|
inf = os.stat(fsenc(abspath))
|
||||||
|
except Exception as ex:
|
||||||
|
self.log("up2k", "stat: " + repr(ex))
|
||||||
|
continue
|
||||||
|
|
||||||
|
if stat.S_ISDIR(inf.st_mode):
|
||||||
|
if abspath in excl or abspath == histdir:
|
||||||
|
continue
|
||||||
|
# self.log("up2k", " dir: {}".format(abspath))
|
||||||
|
self._build_dir(dbw, top, excl, abspath)
|
||||||
|
else:
|
||||||
|
# self.log("up2k", "file: {}".format(abspath))
|
||||||
|
rp = abspath[len(top) :].replace("\\", "/").strip("/")
|
||||||
|
c = dbw[0].execute("select * from up where rp = ?", (rp,))
|
||||||
|
in_db = list(c.fetchall())
|
||||||
|
if in_db:
|
||||||
|
_, dts, dsz, _ = in_db[0]
|
||||||
|
if len(in_db) > 1:
|
||||||
|
m = "WARN: multiple entries: [{}] => [{}] ({})"
|
||||||
|
self.log("up2k", m.format(top, rp, len(in_db)))
|
||||||
|
dts = -1
|
||||||
|
|
||||||
|
if dts == inf.st_mtime and dsz == inf.st_size:
|
||||||
|
continue
|
||||||
|
|
||||||
|
m = "reindex [{}] => [{}] ({}/{}) ({}/{})".format(
|
||||||
|
top, rp, dts, inf.st_mtime, dsz, inf.st_size
|
||||||
|
)
|
||||||
|
self.log("up2k", m)
|
||||||
|
self.db_rm(dbw[0], rp)
|
||||||
|
dbw[1] += 1
|
||||||
|
in_db = None
|
||||||
|
|
||||||
|
self.log("up2k", "file: {}".format(abspath))
|
||||||
|
try:
|
||||||
|
hashes = self._hashlist_from_file(abspath)
|
||||||
|
except Exception as ex:
|
||||||
|
self.log("up2k", "hash: " + repr(ex))
|
||||||
|
continue
|
||||||
|
|
||||||
|
wark = self._wark_from_hashlist(inf.st_size, hashes)
|
||||||
|
self.db_add(dbw[0], wark, rp, inf.st_mtime, inf.st_size)
|
||||||
|
dbw[1] += 1
|
||||||
|
if dbw[1] > 1024:
|
||||||
|
dbw[0].commit()
|
||||||
|
dbw[1] = 0
|
||||||
|
|
||||||
|
def _drop_lost(self, db, top):
|
||||||
|
rm = []
|
||||||
|
c = db.execute("select * from up")
|
||||||
|
for dwark, dts, dsz, drp in c:
|
||||||
|
abspath = os.path.join(top, drp)
|
||||||
|
try:
|
||||||
|
if not os.path.exists(fsenc(abspath)):
|
||||||
|
rm.append(drp)
|
||||||
|
except Exception as ex:
|
||||||
|
self.log("up2k", "stat-rm: " + repr(ex))
|
||||||
|
|
||||||
|
if not rm:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.log("up2k", "forgetting {} deleted files".format(len(rm)))
|
||||||
|
for rp in rm:
|
||||||
|
self.db_rm(db, rp)
|
||||||
|
|
||||||
|
def _open_db(self, db_path):
|
||||||
|
conn = sqlite3.connect(db_path, check_same_thread=False)
|
||||||
|
try:
|
||||||
|
c = conn.execute(r"select * from kv where k = 'sver'")
|
||||||
|
rows = c.fetchall()
|
||||||
|
if rows:
|
||||||
|
ver = rows[0][1]
|
||||||
|
else:
|
||||||
|
self.log("up2k", "WARN: no sver in kv, DB corrupt?")
|
||||||
|
ver = "unknown"
|
||||||
|
|
||||||
|
if ver == "1":
|
||||||
|
try:
|
||||||
|
nfiles = next(conn.execute("select count(w) from up"))[0]
|
||||||
|
self.log("up2k", "found DB at {} |{}|".format(db_path, nfiles))
|
||||||
|
return conn
|
||||||
|
except Exception as ex:
|
||||||
|
m = "WARN: could not list files, DB corrupt?\n " + repr(ex)
|
||||||
|
self.log("up2k", m)
|
||||||
|
|
||||||
|
m = "REPLACING unsupported DB (v.{}) at {}".format(ver, db_path)
|
||||||
|
self.log("up2k", m)
|
||||||
|
conn.close()
|
||||||
|
os.unlink(db_path)
|
||||||
|
conn = sqlite3.connect(db_path, check_same_thread=False)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# sqlite is variable-width only, no point in using char/nchar/varchar
|
||||||
|
for cmd in [
|
||||||
|
r"create table kv (k text, v text)",
|
||||||
|
r"create table up (w text, mt int, sz int, rp text)",
|
||||||
|
r"insert into kv values ('sver', '1')",
|
||||||
|
r"create index up_w on up(w)",
|
||||||
|
]:
|
||||||
|
conn.execute(cmd)
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
self.log("up2k", "created DB at {}".format(db_path))
|
||||||
|
return conn
|
||||||
|
|
||||||
def handle_json(self, cj):
|
def handle_json(self, cj):
|
||||||
|
self.register_vpath(cj["ptop"])
|
||||||
cj["name"] = sanitize_fn(cj["name"])
|
cj["name"] = sanitize_fn(cj["name"])
|
||||||
|
cj["poke"] = time.time()
|
||||||
wark = self._get_wark(cj)
|
wark = self._get_wark(cj)
|
||||||
now = time.time()
|
now = time.time()
|
||||||
|
job = None
|
||||||
with self.mutex:
|
with self.mutex:
|
||||||
# TODO use registry persistence here to symlink any matching wark
|
db = self.db.get(cj["ptop"], None)
|
||||||
if wark in self.registry:
|
reg = self.registry[cj["ptop"]]
|
||||||
job = self.registry[wark]
|
if db:
|
||||||
if job["rdir"] != cj["rdir"] or job["name"] != cj["name"]:
|
cur = db.execute(r"select * from up where w = ?", (wark,))
|
||||||
src = os.path.join(job["rdir"], job["name"])
|
for _, dtime, dsize, dp_rel in cur:
|
||||||
dst = os.path.join(cj["rdir"], cj["name"])
|
dp_abs = os.path.join(cj["ptop"], dp_rel).replace("\\", "/")
|
||||||
|
# relying on path.exists to return false on broken symlinks
|
||||||
|
if os.path.exists(fsenc(dp_abs)):
|
||||||
|
try:
|
||||||
|
prel, name = dp_rel.rsplit("/", 1)
|
||||||
|
except:
|
||||||
|
prel = ""
|
||||||
|
name = dp_rel
|
||||||
|
|
||||||
|
job = {
|
||||||
|
"name": name,
|
||||||
|
"prel": prel,
|
||||||
|
"vtop": cj["vtop"],
|
||||||
|
"ptop": cj["ptop"],
|
||||||
|
"flag": cj["flag"],
|
||||||
|
"size": dsize,
|
||||||
|
"lmod": dtime,
|
||||||
|
"hash": [],
|
||||||
|
"need": [],
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
if job and wark in reg:
|
||||||
|
del reg[wark]
|
||||||
|
|
||||||
|
if job or wark in reg:
|
||||||
|
job = job or reg[wark]
|
||||||
|
if job["prel"] == cj["prel"] and job["name"] == cj["name"]:
|
||||||
|
# ensure the files haven't been deleted manually
|
||||||
|
names = [job[x] for x in ["name", "tnam"] if x in job]
|
||||||
|
for fn in names:
|
||||||
|
path = os.path.join(job["ptop"], job["prel"], fn)
|
||||||
|
if not os.path.exists(path):
|
||||||
|
job = None
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
# file contents match, but not the path
|
||||||
|
src = os.path.join(job["ptop"], job["prel"], job["name"])
|
||||||
|
dst = os.path.join(cj["ptop"], cj["prel"], cj["name"])
|
||||||
|
vsrc = os.path.join(job["vtop"], job["prel"], job["name"])
|
||||||
|
vsrc = vsrc.replace("\\", "/") # just for prints anyways
|
||||||
if job["need"]:
|
if job["need"]:
|
||||||
self.log("up2k", "unfinished:\n {0}\n {1}".format(src, dst))
|
self.log("up2k", "unfinished:\n {0}\n {1}".format(src, dst))
|
||||||
err = "partial upload exists at a different location; please resume uploading here instead:\n{0}{1} ".format(
|
err = "partial upload exists at a different location; please resume uploading here instead:\n"
|
||||||
job["vdir"], job["name"]
|
err += vsrc + " "
|
||||||
)
|
raise Pebkac(400, err)
|
||||||
|
elif "nodupe" in job["flag"]:
|
||||||
|
self.log("up2k", "dupe-reject:\n {0}\n {1}".format(src, dst))
|
||||||
|
err = "upload rejected, file already exists:\n " + vsrc + " "
|
||||||
raise Pebkac(400, err)
|
raise Pebkac(400, err)
|
||||||
else:
|
else:
|
||||||
# symlink to the client-provided name,
|
# symlink to the client-provided name,
|
||||||
# returning the previous upload info
|
# returning the previous upload info
|
||||||
job = deepcopy(job)
|
job = deepcopy(job)
|
||||||
suffix = self._suffix(dst, now, job["addr"])
|
for k in ["ptop", "vtop", "prel"]:
|
||||||
job["name"] = cj["name"] + suffix
|
job[k] = cj[k]
|
||||||
self._symlink(src, dst + suffix)
|
|
||||||
else:
|
pdir = os.path.join(cj["ptop"], cj["prel"])
|
||||||
|
job["name"] = self._untaken(pdir, cj["name"], now, cj["addr"])
|
||||||
|
dst = os.path.join(job["ptop"], job["prel"], job["name"])
|
||||||
|
os.unlink(fsenc(dst)) # TODO ed pls
|
||||||
|
self._symlink(src, dst)
|
||||||
|
|
||||||
|
if not job:
|
||||||
job = {
|
job = {
|
||||||
"wark": wark,
|
"wark": wark,
|
||||||
"t0": now,
|
"t0": now,
|
||||||
"addr": cj["addr"],
|
|
||||||
"vdir": cj["vdir"],
|
|
||||||
"rdir": cj["rdir"],
|
|
||||||
# client-provided, sanitized by _get_wark:
|
|
||||||
"name": cj["name"],
|
|
||||||
"size": cj["size"],
|
|
||||||
"lmod": cj["lmod"],
|
|
||||||
"hash": deepcopy(cj["hash"]),
|
"hash": deepcopy(cj["hash"]),
|
||||||
|
"need": [],
|
||||||
}
|
}
|
||||||
|
# client-provided, sanitized by _get_wark: name, size, lmod
|
||||||
path = os.path.join(job["rdir"], job["name"])
|
for k in [
|
||||||
job["name"] += self._suffix(path, now, cj["addr"])
|
"addr",
|
||||||
|
"vtop",
|
||||||
|
"ptop",
|
||||||
|
"prel",
|
||||||
|
"flag",
|
||||||
|
"name",
|
||||||
|
"size",
|
||||||
|
"lmod",
|
||||||
|
"poke",
|
||||||
|
]:
|
||||||
|
job[k] = cj[k]
|
||||||
|
|
||||||
# one chunk may occur multiple times in a file;
|
# one chunk may occur multiple times in a file;
|
||||||
# filter to unique values for the list of missing chunks
|
# filter to unique values for the list of missing chunks
|
||||||
# (preserve order to reduce disk thrashing)
|
# (preserve order to reduce disk thrashing)
|
||||||
job["need"] = []
|
|
||||||
lut = {}
|
lut = {}
|
||||||
for k in cj["hash"]:
|
for k in cj["hash"]:
|
||||||
if k not in lut:
|
if k not in lut:
|
||||||
@@ -108,13 +367,12 @@ class Up2k(object):
|
|||||||
"wark": wark,
|
"wark": wark,
|
||||||
}
|
}
|
||||||
|
|
||||||
def _suffix(self, fpath, ts, ip):
|
def _untaken(self, fdir, fname, ts, ip):
|
||||||
# TODO broker which avoid this race and
|
# TODO broker which avoid this race and
|
||||||
# provides a new filename if taken (same as bup)
|
# provides a new filename if taken (same as bup)
|
||||||
if not os.path.exists(fsenc(fpath)):
|
suffix = ".{:.6f}-{}".format(ts, ip)
|
||||||
return ""
|
with ren_open(fname, "wb", fdir=fdir, suffix=suffix) as f:
|
||||||
|
return f["orz"][1]
|
||||||
return ".{:.6f}-{}".format(ts, ip)
|
|
||||||
|
|
||||||
def _symlink(self, src, dst):
|
def _symlink(self, src, dst):
|
||||||
# TODO store this in linktab so we never delete src if there are links to it
|
# TODO store this in linktab so we never delete src if there are links to it
|
||||||
@@ -141,40 +399,58 @@ class Up2k(object):
|
|||||||
lsrc = "../" * (len(lsrc) - 1) + "/".join(lsrc)
|
lsrc = "../" * (len(lsrc) - 1) + "/".join(lsrc)
|
||||||
os.symlink(fsenc(lsrc), fsenc(ldst))
|
os.symlink(fsenc(lsrc), fsenc(ldst))
|
||||||
except (AttributeError, OSError) as ex:
|
except (AttributeError, OSError) as ex:
|
||||||
self.log("up2k", "cannot symlink; creating copy")
|
self.log("up2k", "cannot symlink; creating copy: " + repr(ex))
|
||||||
shutil.copy2(fsenc(src), fsenc(dst))
|
shutil.copy2(fsenc(src), fsenc(dst))
|
||||||
|
|
||||||
def handle_chunk(self, wark, chash):
|
def handle_chunk(self, ptop, wark, chash):
|
||||||
with self.mutex:
|
with self.mutex:
|
||||||
job = self.registry.get(wark)
|
job = self.registry[ptop].get(wark, None)
|
||||||
if not job:
|
if not job:
|
||||||
raise Pebkac(404, "unknown wark")
|
raise Pebkac(400, "unknown wark")
|
||||||
|
|
||||||
if chash not in job["need"]:
|
if chash not in job["need"]:
|
||||||
raise Pebkac(200, "already got that but thanks??")
|
raise Pebkac(200, "already got that but thanks??")
|
||||||
|
|
||||||
nchunk = [n for n, v in enumerate(job["hash"]) if v == chash]
|
nchunk = [n for n, v in enumerate(job["hash"]) if v == chash]
|
||||||
if not nchunk:
|
if not nchunk:
|
||||||
raise Pebkac(404, "unknown chunk")
|
raise Pebkac(400, "unknown chunk")
|
||||||
|
|
||||||
|
job["poke"] = time.time()
|
||||||
|
|
||||||
chunksize = self._get_chunksize(job["size"])
|
chunksize = self._get_chunksize(job["size"])
|
||||||
ofs = [chunksize * x for x in nchunk]
|
ofs = [chunksize * x for x in nchunk]
|
||||||
|
|
||||||
path = os.path.join(job["rdir"], job["name"])
|
path = os.path.join(job["ptop"], job["prel"], job["tnam"])
|
||||||
|
|
||||||
return [chunksize, ofs, path, job["lmod"]]
|
return [chunksize, ofs, path, job["lmod"]]
|
||||||
|
|
||||||
def confirm_chunk(self, wark, chash):
|
def confirm_chunk(self, ptop, wark, chash):
|
||||||
with self.mutex:
|
with self.mutex:
|
||||||
job = self.registry[wark]
|
job = self.registry[ptop][wark]
|
||||||
|
pdir = os.path.join(job["ptop"], job["prel"])
|
||||||
|
src = os.path.join(pdir, job["tnam"])
|
||||||
|
dst = os.path.join(pdir, job["name"])
|
||||||
|
|
||||||
job["need"].remove(chash)
|
job["need"].remove(chash)
|
||||||
ret = len(job["need"])
|
ret = len(job["need"])
|
||||||
|
if ret > 0:
|
||||||
|
return ret, src
|
||||||
|
|
||||||
if WINDOWS and ret == 0:
|
atomic_move(src, dst)
|
||||||
path = os.path.join(job["rdir"], job["name"])
|
|
||||||
self.lastmod_q.put([path, (int(time.time()), int(job["lmod"]))])
|
|
||||||
|
|
||||||
return ret
|
if WINDOWS:
|
||||||
|
self.lastmod_q.put([dst, (int(time.time()), int(job["lmod"]))])
|
||||||
|
|
||||||
|
db = self.db.get(job["ptop"], None)
|
||||||
|
if db:
|
||||||
|
rp = os.path.join(job["prel"], job["name"]).replace("\\", "/")
|
||||||
|
self.db_rm(db, rp)
|
||||||
|
self.db_add(db, job["wark"], rp, job["lmod"], job["size"])
|
||||||
|
db.commit()
|
||||||
|
del self.registry[ptop][wark]
|
||||||
|
# in-memory registry is reserved for unfinished uploads
|
||||||
|
|
||||||
|
return ret, dst
|
||||||
|
|
||||||
def _get_chunksize(self, filesize):
|
def _get_chunksize(self, filesize):
|
||||||
chunksize = 1024 * 1024
|
chunksize = 1024 * 1024
|
||||||
@@ -188,6 +464,13 @@ class Up2k(object):
|
|||||||
chunksize += stepsize
|
chunksize += stepsize
|
||||||
stepsize *= mul
|
stepsize *= mul
|
||||||
|
|
||||||
|
def db_rm(self, db, rp):
|
||||||
|
db.execute("delete from up where rp = ?", (rp,))
|
||||||
|
|
||||||
|
def db_add(self, db, wark, rp, ts, sz):
|
||||||
|
v = (wark, ts, sz, rp)
|
||||||
|
db.execute("insert into up values (?,?,?,?)", v)
|
||||||
|
|
||||||
def _get_wark(self, cj):
|
def _get_wark(self, cj):
|
||||||
if len(cj["name"]) > 1024 or len(cj["hash"]) > 512 * 1024: # 16TiB
|
if len(cj["name"]) > 1024 or len(cj["hash"]) > 512 * 1024: # 16TiB
|
||||||
raise Pebkac(400, "name or numchunks not according to spec")
|
raise Pebkac(400, "name or numchunks not according to spec")
|
||||||
@@ -204,9 +487,13 @@ class Up2k(object):
|
|||||||
except:
|
except:
|
||||||
cj["lmod"] = int(time.time())
|
cj["lmod"] = int(time.time())
|
||||||
|
|
||||||
# server-reproducible file identifier, independent of name or location
|
wark = self._wark_from_hashlist(cj["size"], cj["hash"])
|
||||||
ident = [self.salt, str(cj["size"])]
|
return wark
|
||||||
ident.extend(cj["hash"])
|
|
||||||
|
def _wark_from_hashlist(self, filesize, hashes):
|
||||||
|
""" server-reproducible file identifier, independent of name or location """
|
||||||
|
ident = [self.salt, str(filesize)]
|
||||||
|
ident.extend(hashes)
|
||||||
ident = "\n".join(ident)
|
ident = "\n".join(ident)
|
||||||
|
|
||||||
hasher = hashlib.sha512()
|
hasher = hashlib.sha512()
|
||||||
@@ -216,10 +503,38 @@ class Up2k(object):
|
|||||||
wark = base64.urlsafe_b64encode(digest)
|
wark = base64.urlsafe_b64encode(digest)
|
||||||
return wark.decode("utf-8").rstrip("=")
|
return wark.decode("utf-8").rstrip("=")
|
||||||
|
|
||||||
|
def _hashlist_from_file(self, path):
|
||||||
|
fsz = os.path.getsize(path)
|
||||||
|
csz = self._get_chunksize(fsz)
|
||||||
|
ret = []
|
||||||
|
with open(path, "rb", 512 * 1024) as f:
|
||||||
|
while fsz > 0:
|
||||||
|
hashobj = hashlib.sha512()
|
||||||
|
rem = min(csz, fsz)
|
||||||
|
fsz -= rem
|
||||||
|
while rem > 0:
|
||||||
|
buf = f.read(min(rem, 64 * 1024))
|
||||||
|
if not buf:
|
||||||
|
raise Exception("EOF at " + str(f.tell()))
|
||||||
|
|
||||||
|
hashobj.update(buf)
|
||||||
|
rem -= len(buf)
|
||||||
|
|
||||||
|
digest = hashobj.digest()[:32]
|
||||||
|
digest = base64.urlsafe_b64encode(digest)
|
||||||
|
ret.append(digest.decode("utf-8").rstrip("="))
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
def _new_upload(self, job):
|
def _new_upload(self, job):
|
||||||
self.registry[job["wark"]] = job
|
self.registry[job["ptop"]][job["wark"]] = job
|
||||||
path = os.path.join(job["rdir"], job["name"])
|
pdir = os.path.join(job["ptop"], job["prel"])
|
||||||
with open(fsenc(path), "wb") as f:
|
job["name"] = self._untaken(pdir, job["name"], job["t0"], job["addr"])
|
||||||
|
|
||||||
|
tnam = job["name"] + ".PARTIAL"
|
||||||
|
suffix = ".{:.6f}-{}".format(job["t0"], job["addr"])
|
||||||
|
with ren_open(tnam, "wb", fdir=pdir, suffix=suffix) as f:
|
||||||
|
f, job["tnam"] = f["orz"]
|
||||||
f.seek(job["size"] - 1)
|
f.seek(job["size"] - 1)
|
||||||
f.write(b"e")
|
f.write(b"e")
|
||||||
|
|
||||||
@@ -236,3 +551,58 @@ class Up2k(object):
|
|||||||
os.utime(fsenc(path), times)
|
os.utime(fsenc(path), times)
|
||||||
except:
|
except:
|
||||||
self.log("lmod", "failed to utime ({}, {})".format(path, times))
|
self.log("lmod", "failed to utime ({}, {})".format(path, times))
|
||||||
|
|
||||||
|
def _snapshot(self):
|
||||||
|
persist_interval = 30 # persist unfinished uploads index every 30 sec
|
||||||
|
discard_interval = 3600 # drop unfinished uploads after 1 hour inactivity
|
||||||
|
prev = {}
|
||||||
|
while True:
|
||||||
|
time.sleep(persist_interval)
|
||||||
|
with self.mutex:
|
||||||
|
for k, reg in self.registry.items():
|
||||||
|
self._snap_reg(prev, k, reg, discard_interval)
|
||||||
|
|
||||||
|
def _snap_reg(self, prev, k, reg, discard_interval):
|
||||||
|
now = time.time()
|
||||||
|
rm = [x for x in reg.values() if now - x["poke"] > discard_interval]
|
||||||
|
if rm:
|
||||||
|
m = "dropping {} abandoned uploads in {}".format(len(rm), k)
|
||||||
|
vis = [self._vis_job_progress(x) for x in rm]
|
||||||
|
self.log("up2k", "\n".join([m] + vis))
|
||||||
|
for job in rm:
|
||||||
|
del reg[job["wark"]]
|
||||||
|
try:
|
||||||
|
# remove the filename reservation
|
||||||
|
path = os.path.join(job["ptop"], job["prel"], job["name"])
|
||||||
|
if os.path.getsize(path) == 0:
|
||||||
|
os.unlink(path)
|
||||||
|
|
||||||
|
if len(job["hash"]) == len(job["need"]):
|
||||||
|
# PARTIAL is empty, delete that too
|
||||||
|
path = os.path.join(job["ptop"], job["prel"], job["tnam"])
|
||||||
|
os.unlink(path)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
path = os.path.join(k, ".hist", "up2k.snap")
|
||||||
|
if not reg:
|
||||||
|
if k not in prev or prev[k] is not None:
|
||||||
|
prev[k] = None
|
||||||
|
if os.path.exists(path):
|
||||||
|
os.unlink(path)
|
||||||
|
return
|
||||||
|
|
||||||
|
newest = max(x["poke"] for _, x in reg.items()) if reg else 0
|
||||||
|
etag = [len(reg), newest]
|
||||||
|
if etag == prev.get(k, None):
|
||||||
|
return
|
||||||
|
|
||||||
|
path2 = "{}.{}".format(path, os.getpid())
|
||||||
|
j = json.dumps(reg, indent=2, sort_keys=True).encode("utf-8")
|
||||||
|
with gzip.GzipFile(path2, "wb") as f:
|
||||||
|
f.write(j)
|
||||||
|
|
||||||
|
atomic_move(path2, path)
|
||||||
|
|
||||||
|
self.log("up2k", "snap: {} |{}|".format(path, len(reg.keys())))
|
||||||
|
prev[k] = etag
|
||||||
|
|||||||
@@ -2,14 +2,17 @@
|
|||||||
from __future__ import print_function, unicode_literals
|
from __future__ import print_function, unicode_literals
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
import os
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import base64
|
import base64
|
||||||
|
import select
|
||||||
import struct
|
import struct
|
||||||
import hashlib
|
import hashlib
|
||||||
import platform
|
import platform
|
||||||
import threading
|
import threading
|
||||||
import mimetypes
|
import mimetypes
|
||||||
|
import contextlib
|
||||||
import subprocess as sp # nosec
|
import subprocess as sp # nosec
|
||||||
|
|
||||||
from .__init__ import PY2, WINDOWS
|
from .__init__ import PY2, WINDOWS
|
||||||
@@ -96,6 +99,80 @@ class Unrecv(object):
|
|||||||
self.buf = buf + self.buf
|
self.buf = buf + self.buf
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def ren_open(fname, *args, **kwargs):
|
||||||
|
fdir = kwargs.pop("fdir", None)
|
||||||
|
suffix = kwargs.pop("suffix", None)
|
||||||
|
|
||||||
|
if fname == os.devnull:
|
||||||
|
with open(fname, *args, **kwargs) as f:
|
||||||
|
yield {"orz": [f, fname]}
|
||||||
|
return
|
||||||
|
|
||||||
|
orig_name = fname
|
||||||
|
bname = fname
|
||||||
|
ext = ""
|
||||||
|
while True:
|
||||||
|
ofs = bname.rfind(".")
|
||||||
|
if ofs < 0 or ofs < len(bname) - 7:
|
||||||
|
# doesn't look like an extension anymore
|
||||||
|
break
|
||||||
|
|
||||||
|
ext = bname[ofs:] + ext
|
||||||
|
bname = bname[:ofs]
|
||||||
|
|
||||||
|
b64 = ""
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
if fdir:
|
||||||
|
fpath = os.path.join(fdir, fname)
|
||||||
|
else:
|
||||||
|
fpath = fname
|
||||||
|
|
||||||
|
if suffix and os.path.exists(fpath):
|
||||||
|
fpath += suffix
|
||||||
|
fname += suffix
|
||||||
|
ext += suffix
|
||||||
|
|
||||||
|
with open(fsenc(fpath), *args, **kwargs) as f:
|
||||||
|
if b64:
|
||||||
|
fp2 = "fn-trunc.{}.txt".format(b64)
|
||||||
|
fp2 = os.path.join(fdir, fp2)
|
||||||
|
with open(fsenc(fp2), "wb") as f2:
|
||||||
|
f2.write(orig_name.encode("utf-8"))
|
||||||
|
|
||||||
|
yield {"orz": [f, fname]}
|
||||||
|
return
|
||||||
|
|
||||||
|
except OSError as ex_:
|
||||||
|
ex = ex_
|
||||||
|
if ex.errno != 36:
|
||||||
|
raise
|
||||||
|
|
||||||
|
if not b64:
|
||||||
|
b64 = (bname + ext).encode("utf-8", "replace")
|
||||||
|
b64 = hashlib.sha512(b64).digest()[:12]
|
||||||
|
b64 = base64.urlsafe_b64encode(b64).decode("utf-8").rstrip("=")
|
||||||
|
|
||||||
|
badlen = len(fname)
|
||||||
|
while len(fname) >= badlen:
|
||||||
|
if len(bname) < 8:
|
||||||
|
raise ex
|
||||||
|
|
||||||
|
if len(bname) > len(ext):
|
||||||
|
# drop the last letter of the filename
|
||||||
|
bname = bname[:-1]
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
# drop the leftmost sub-extension
|
||||||
|
_, ext = ext.split(".", 1)
|
||||||
|
except:
|
||||||
|
# okay do the first letter then
|
||||||
|
ext = "." + ext[2:]
|
||||||
|
|
||||||
|
fname = "{}~{}{}".format(bname, b64, ext)
|
||||||
|
|
||||||
|
|
||||||
class MultipartParser(object):
|
class MultipartParser(object):
|
||||||
def __init__(self, log_func, sr, http_headers):
|
def __init__(self, log_func, sr, http_headers):
|
||||||
self.sr = sr
|
self.sr = sr
|
||||||
@@ -472,6 +549,16 @@ else:
|
|||||||
fsdec = w8dec
|
fsdec = w8dec
|
||||||
|
|
||||||
|
|
||||||
|
def atomic_move(src, dst):
|
||||||
|
if not PY2:
|
||||||
|
os.replace(src, dst)
|
||||||
|
else:
|
||||||
|
if os.path.exists(dst):
|
||||||
|
os.unlink(dst)
|
||||||
|
|
||||||
|
os.rename(src, dst)
|
||||||
|
|
||||||
|
|
||||||
def read_socket(sr, total_size):
|
def read_socket(sr, total_size):
|
||||||
remains = total_size
|
remains = total_size
|
||||||
while remains > 0:
|
while remains > 0:
|
||||||
@@ -515,6 +602,46 @@ def hashcopy(actor, fin, fout):
|
|||||||
return tlen, hashobj.hexdigest(), digest_b64
|
return tlen, hashobj.hexdigest(), digest_b64
|
||||||
|
|
||||||
|
|
||||||
|
def sendfile_py(lower, upper, f, s):
|
||||||
|
remains = upper - lower
|
||||||
|
f.seek(lower)
|
||||||
|
while remains > 0:
|
||||||
|
# time.sleep(0.01)
|
||||||
|
buf = f.read(min(4096, remains))
|
||||||
|
if not buf:
|
||||||
|
return remains
|
||||||
|
|
||||||
|
try:
|
||||||
|
s.sendall(buf)
|
||||||
|
remains -= len(buf)
|
||||||
|
except:
|
||||||
|
return remains
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def sendfile_kern(lower, upper, f, s):
|
||||||
|
out_fd = s.fileno()
|
||||||
|
in_fd = f.fileno()
|
||||||
|
ofs = lower
|
||||||
|
while ofs < upper:
|
||||||
|
try:
|
||||||
|
req = min(2 ** 30, upper - ofs)
|
||||||
|
select.select([], [out_fd], [], 10)
|
||||||
|
n = os.sendfile(out_fd, in_fd, ofs, req)
|
||||||
|
except Exception as ex:
|
||||||
|
# print("sendfile: " + repr(ex))
|
||||||
|
n = 0
|
||||||
|
|
||||||
|
if n <= 0:
|
||||||
|
return upper - ofs
|
||||||
|
|
||||||
|
ofs += n
|
||||||
|
# print("sendfile: ok, sent {} now, {} total, {} remains".format(n, ofs - lower, upper - ofs))
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def unescape_cookie(orig):
|
def unescape_cookie(orig):
|
||||||
# mw=idk; doot=qwe%2Crty%3Basd+fgh%2Bjkl%25zxc%26vbn # qwe,rty;asd fgh+jkl%zxc&vbn
|
# mw=idk; doot=qwe%2Crty%3Basd+fgh%2Bjkl%25zxc%26vbn # qwe,rty;asd fgh+jkl%zxc&vbn
|
||||||
ret = ""
|
ret = ""
|
||||||
@@ -595,3 +722,6 @@ class Pebkac(Exception):
|
|||||||
def __init__(self, code, msg=None):
|
def __init__(self, code, msg=None):
|
||||||
super(Pebkac, self).__init__(msg or HTTPCODE[code])
|
super(Pebkac, self).__init__(msg or HTTPCODE[code])
|
||||||
self.code = code
|
self.code = code
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "Pebkac({}, {})".format(self.code, repr(self.args))
|
||||||
|
|||||||
12
copyparty/web/Makefile
Normal file
12
copyparty/web/Makefile
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# run me to zopfli all the static files
|
||||||
|
# which should help on really slow connections
|
||||||
|
# but then why are you using copyparty in the first place
|
||||||
|
|
||||||
|
pk: $(addsuffix .gz, $(wildcard *.js *.css))
|
||||||
|
un: $(addsuffix .un, $(wildcard *.gz))
|
||||||
|
|
||||||
|
%.gz: %
|
||||||
|
pigz -11 -J 34 -I 5730 $<
|
||||||
|
|
||||||
|
%.un: %
|
||||||
|
pigz -d $<
|
||||||
@@ -8,7 +8,14 @@ function dbg(msg) {
|
|||||||
|
|
||||||
function ev(e) {
|
function ev(e) {
|
||||||
e = e || window.event;
|
e = e || window.event;
|
||||||
e.preventDefault ? e.preventDefault() : (e.returnValue = false);
|
|
||||||
|
if (e.preventDefault)
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
if (e.stopPropagation)
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
e.returnValue = false;
|
||||||
return e;
|
return e;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,9 +32,9 @@ var mp = (function () {
|
|||||||
'tracks': tracks,
|
'tracks': tracks,
|
||||||
'cover_url': ''
|
'cover_url': ''
|
||||||
};
|
};
|
||||||
var re_audio = new RegExp('\.(opus|ogg|m4a|aac|mp3|wav|flac)$', 'i');
|
var re_audio = /\.(opus|ogg|m4a|aac|mp3|wav|flac)$/i;
|
||||||
|
|
||||||
var trs = document.getElementById('files').getElementsByTagName('tbody')[0].getElementsByTagName('tr');
|
var trs = ebi('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++) {
|
||||||
var tds = trs[a].getElementsByTagName('td');
|
var tds = trs[a].getElementsByTagName('td');
|
||||||
var link = tds[1].getElementsByTagName('a')[0];
|
var link = tds[1].getElementsByTagName('a')[0];
|
||||||
@@ -70,8 +77,8 @@ var mp = (function () {
|
|||||||
// toggle player widget
|
// toggle player widget
|
||||||
var widget = (function () {
|
var widget = (function () {
|
||||||
var ret = {};
|
var ret = {};
|
||||||
var widget = document.getElementById('widget');
|
var widget = ebi('widget');
|
||||||
var wtoggle = document.getElementById('wtoggle');
|
var wtoggle = ebi('wtoggle');
|
||||||
var touchmode = false;
|
var touchmode = false;
|
||||||
var side_open = false;
|
var side_open = false;
|
||||||
var was_paused = true;
|
var was_paused = true;
|
||||||
@@ -315,8 +322,12 @@ var vbar = (function () {
|
|||||||
var rect = pbar.pcan.getBoundingClientRect();
|
var rect = pbar.pcan.getBoundingClientRect();
|
||||||
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;
|
||||||
|
var seek = mp.au.duration * mul;
|
||||||
|
console.log('seek: ' + seek);
|
||||||
|
if (!isFinite(seek))
|
||||||
|
return;
|
||||||
|
|
||||||
mp.au.currentTime = mp.au.duration * mul;
|
mp.au.currentTime = seek;
|
||||||
|
|
||||||
if (mp.au === mp.au_native)
|
if (mp.au === mp.au_native)
|
||||||
// hack: ogv.js breaks on .play() during playback
|
// hack: ogv.js breaks on .play() during playback
|
||||||
@@ -443,7 +454,8 @@ function play(tid, call_depth) {
|
|||||||
mp.au.tid = tid;
|
mp.au.tid = tid;
|
||||||
mp.au.src = url;
|
mp.au.src = url;
|
||||||
mp.au.volume = mp.expvol();
|
mp.au.volume = mp.expvol();
|
||||||
setclass('trk' + tid, 'play act');
|
var oid = 'trk' + tid;
|
||||||
|
setclass(oid, 'play act');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (hack_attempt_play)
|
if (hack_attempt_play)
|
||||||
@@ -452,7 +464,11 @@ function play(tid, call_depth) {
|
|||||||
if (mp.au.paused)
|
if (mp.au.paused)
|
||||||
autoplay_blocked();
|
autoplay_blocked();
|
||||||
|
|
||||||
location.hash = 'trk' + tid;
|
var o = ebi(oid);
|
||||||
|
o.setAttribute('id', 'thx_js');
|
||||||
|
location.hash = oid;
|
||||||
|
o.setAttribute('id', oid);
|
||||||
|
|
||||||
pbar.drawbuf();
|
pbar.drawbuf();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -468,7 +484,6 @@ function play(tid, call_depth) {
|
|||||||
function evau_error(e) {
|
function evau_error(e) {
|
||||||
var err = '';
|
var err = '';
|
||||||
var eplaya = (e && e.target) || (window.event && window.event.srcElement);
|
var eplaya = (e && e.target) || (window.event && window.event.srcElement);
|
||||||
var url = eplaya.src;
|
|
||||||
|
|
||||||
switch (eplaya.error.code) {
|
switch (eplaya.error.code) {
|
||||||
case eplaya.error.MEDIA_ERR_ABORTED:
|
case eplaya.error.MEDIA_ERR_ABORTED:
|
||||||
@@ -516,7 +531,7 @@ function unblocked() {
|
|||||||
|
|
||||||
|
|
||||||
// show ui to manually start playback of a linked song
|
// show ui to manually start playback of a linked song
|
||||||
function autoplay_blocked(tid) {
|
function autoplay_blocked() {
|
||||||
show_modal(
|
show_modal(
|
||||||
'<div id="blk_play"><a href="#" id="blk_go"></a></div>' +
|
'<div id="blk_play"><a href="#" id="blk_go"></a></div>' +
|
||||||
'<div id="blk_abrt"><a href="#" id="blk_na">Cancel<br />(show file list)</a></div>');
|
'<div id="blk_abrt"><a href="#" id="blk_na">Cancel<br />(show file list)</a></div>');
|
||||||
|
|||||||
@@ -126,7 +126,8 @@ write markdown (most html is 🙆 too)
|
|||||||
var last_modified = {{ lastmod }};
|
var last_modified = {{ lastmod }};
|
||||||
var md_opt = {
|
var md_opt = {
|
||||||
link_md_as_html: false,
|
link_md_as_html: false,
|
||||||
allow_plugins: {{ md_plug }}
|
allow_plugins: {{ md_plug }},
|
||||||
|
modpoll_freq: {{ md_chk_rate }}
|
||||||
};
|
};
|
||||||
|
|
||||||
(function () {
|
(function () {
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
var dom_toc = document.getElementById('toc');
|
var dom_toc = ebi('toc');
|
||||||
var dom_wrap = document.getElementById('mw');
|
var dom_wrap = ebi('mw');
|
||||||
var dom_hbar = document.getElementById('mh');
|
var dom_hbar = ebi('mh');
|
||||||
var dom_nav = document.getElementById('mn');
|
var dom_nav = ebi('mn');
|
||||||
var dom_pre = document.getElementById('mp');
|
var dom_pre = ebi('mp');
|
||||||
var dom_src = document.getElementById('mt');
|
var dom_src = ebi('mt');
|
||||||
var dom_navtgl = document.getElementById('navtoggle');
|
var dom_navtgl = ebi('navtoggle');
|
||||||
|
|
||||||
|
|
||||||
// chrome 49 needs this
|
// chrome 49 needs this
|
||||||
@@ -161,7 +161,7 @@ function copydom(src, dst, lv) {
|
|||||||
|
|
||||||
|
|
||||||
function md_plug_err(ex, js) {
|
function md_plug_err(ex, js) {
|
||||||
var errbox = document.getElementById('md_errbox');
|
var errbox = ebi('md_errbox');
|
||||||
if (errbox)
|
if (errbox)
|
||||||
errbox.parentNode.removeChild(errbox);
|
errbox.parentNode.removeChild(errbox);
|
||||||
|
|
||||||
@@ -299,7 +299,7 @@ function convert_markdown(md_text, dest_dom) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// separate <code> for each line in <pre>
|
// separate <code> for each line in <pre>
|
||||||
var nodes = md_dom.getElementsByTagName('pre');
|
nodes = md_dom.getElementsByTagName('pre');
|
||||||
for (var a = nodes.length - 1; a >= 0; a--) {
|
for (var a = nodes.length - 1; a >= 0; a--) {
|
||||||
var el = nodes[a];
|
var el = nodes[a];
|
||||||
|
|
||||||
@@ -367,7 +367,7 @@ function convert_markdown(md_text, dest_dom) {
|
|||||||
|
|
||||||
|
|
||||||
function init_toc() {
|
function init_toc() {
|
||||||
var loader = document.getElementById('ml');
|
var loader = ebi('ml');
|
||||||
loader.parentNode.removeChild(loader);
|
loader.parentNode.removeChild(loader);
|
||||||
|
|
||||||
var anchors = []; // list of toc entries, complex objects
|
var anchors = []; // list of toc entries, complex objects
|
||||||
|
|||||||
@@ -77,32 +77,52 @@ html.dark #mt {
|
|||||||
background: #f97;
|
background: #f97;
|
||||||
border-radius: .15em;
|
border-radius: .15em;
|
||||||
}
|
}
|
||||||
|
html.dark #save.force-save {
|
||||||
|
color: #fca;
|
||||||
|
background: #720;
|
||||||
|
}
|
||||||
#save.disabled {
|
#save.disabled {
|
||||||
opacity: .4;
|
opacity: .4;
|
||||||
}
|
}
|
||||||
|
#helpbox,
|
||||||
|
#toast {
|
||||||
|
background: #f7f7f7;
|
||||||
|
border-radius: .4em;
|
||||||
|
z-index: 9001;
|
||||||
|
}
|
||||||
#helpbox {
|
#helpbox {
|
||||||
display: none;
|
display: none;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
background: #f7f7f7;
|
|
||||||
box-shadow: 0 .5em 2em #777;
|
|
||||||
border-radius: .4em;
|
|
||||||
padding: 2em;
|
padding: 2em;
|
||||||
top: 4em;
|
top: 4em;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
box-shadow: 0 .5em 2em #777;
|
||||||
height: calc(100% - 12em);
|
height: calc(100% - 12em);
|
||||||
left: calc(50% - 15em);
|
left: calc(50% - 15em);
|
||||||
right: 0;
|
right: 0;
|
||||||
width: 30em;
|
width: 30em;
|
||||||
z-index: 9001;
|
|
||||||
}
|
}
|
||||||
#helpclose {
|
#helpclose {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
html.dark #helpbox {
|
html.dark #helpbox {
|
||||||
background: #222;
|
|
||||||
box-shadow: 0 .5em 2em #444;
|
box-shadow: 0 .5em 2em #444;
|
||||||
|
}
|
||||||
|
html.dark #helpbox,
|
||||||
|
html.dark #toast {
|
||||||
|
background: #222;
|
||||||
border: 1px solid #079;
|
border: 1px solid #079;
|
||||||
border-width: 1px 0;
|
border-width: 1px 0;
|
||||||
}
|
}
|
||||||
|
#toast {
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
padding: .6em 0;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 9001;
|
||||||
|
top: 30%;
|
||||||
|
transition: opacity 0.2s ease-in-out;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
# mt {opacity: .5;top:1px}
|
# mt {opacity: .5;top:1px}
|
||||||
|
|||||||
@@ -11,15 +11,15 @@ var js_uni_whitelist = eval('\'' + esc_uni_whitelist + '\'');
|
|||||||
|
|
||||||
|
|
||||||
// dom nodes
|
// dom nodes
|
||||||
var dom_swrap = document.getElementById('mtw');
|
var dom_swrap = ebi('mtw');
|
||||||
var dom_sbs = document.getElementById('sbs');
|
var dom_sbs = ebi('sbs');
|
||||||
var dom_nsbs = document.getElementById('nsbs');
|
var dom_nsbs = ebi('nsbs');
|
||||||
var dom_tbox = document.getElementById('toolsbox');
|
var dom_tbox = ebi('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');
|
||||||
dom_swrap.appendChild(d);
|
dom_swrap.appendChild(d);
|
||||||
d = document.getElementById('mtr');
|
d = ebi('mtr');
|
||||||
// hide behind the textarea (offsetTop is not computed if display:none)
|
// hide behind the textarea (offsetTop is not computed if display:none)
|
||||||
dom_src.style.zIndex = '4';
|
dom_src.style.zIndex = '4';
|
||||||
d.style.zIndex = '3';
|
d.style.zIndex = '3';
|
||||||
@@ -108,7 +108,7 @@ var draw_md = (function () {
|
|||||||
map_src = genmap(dom_ref, map_src);
|
map_src = genmap(dom_ref, map_src);
|
||||||
map_pre = genmap(dom_pre, map_pre);
|
map_pre = genmap(dom_pre, map_pre);
|
||||||
|
|
||||||
cls(document.getElementById('save'), 'disabled', src == server_md);
|
cls(ebi('save'), 'disabled', src == server_md);
|
||||||
|
|
||||||
var t1 = new Date().getTime();
|
var t1 = new Date().getTime();
|
||||||
delay = t1 - t0 > 100 ? 25 : 1;
|
delay = t1 - t0 > 100 ? 25 : 1;
|
||||||
@@ -144,7 +144,7 @@ redraw = (function () {
|
|||||||
onresize();
|
onresize();
|
||||||
}
|
}
|
||||||
function modetoggle() {
|
function modetoggle() {
|
||||||
mode = dom_nsbs.innerHTML;
|
var mode = dom_nsbs.innerHTML;
|
||||||
dom_nsbs.innerHTML = mode == 'editor' ? 'preview' : 'editor';
|
dom_nsbs.innerHTML = mode == 'editor' ? 'preview' : 'editor';
|
||||||
mode += ' single';
|
mode += ' single';
|
||||||
dom_wrap.setAttribute('class', mode);
|
dom_wrap.setAttribute('class', mode);
|
||||||
@@ -223,14 +223,108 @@ redraw = (function () {
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
||||||
|
// modification checker
|
||||||
|
function Modpoll() {
|
||||||
|
this.skip_one = true;
|
||||||
|
this.disabled = false;
|
||||||
|
|
||||||
|
this.periodic = function () {
|
||||||
|
var that = this;
|
||||||
|
setTimeout(function () {
|
||||||
|
that.periodic();
|
||||||
|
}, 1000 * md_opt.modpoll_freq);
|
||||||
|
|
||||||
|
var skip = null;
|
||||||
|
|
||||||
|
if (ebi('toast'))
|
||||||
|
skip = 'toast';
|
||||||
|
|
||||||
|
else if (this.skip_one)
|
||||||
|
skip = 'saved';
|
||||||
|
|
||||||
|
else if (this.disabled)
|
||||||
|
skip = 'disabled';
|
||||||
|
|
||||||
|
if (skip) {
|
||||||
|
console.log('modpoll skip, ' + skip);
|
||||||
|
this.skip_one = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('modpoll...');
|
||||||
|
var url = (document.location + '').split('?')[0] + '?raw&_=' + new Date().getTime();
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.modpoll = this;
|
||||||
|
xhr.open('GET', url, true);
|
||||||
|
xhr.responseType = 'text';
|
||||||
|
xhr.onreadystatechange = this.cb;
|
||||||
|
xhr.send();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cb = function () {
|
||||||
|
if (this.modpoll.disabled || this.modpoll.skip_one) {
|
||||||
|
console.log('modpoll abort');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.readyState != XMLHttpRequest.DONE)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (this.status !== 200) {
|
||||||
|
console.log('modpoll err ' + this.status + ": " + this.responseText);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.responseText)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var server_ref = server_md.replace(/\r/g, '');
|
||||||
|
var server_now = this.responseText.replace(/\r/g, '');
|
||||||
|
|
||||||
|
if (server_ref != server_now) {
|
||||||
|
console.log("modpoll diff |" + server_ref.length + "|, |" + server_now.length + "|");
|
||||||
|
this.modpoll.disabled = true;
|
||||||
|
var msg = [
|
||||||
|
"The document has changed on the server.<br />" +
|
||||||
|
"The changes will NOT be loaded into your editor automatically.",
|
||||||
|
|
||||||
|
"Press F5 or CTRL-R to refresh the page,<br />" +
|
||||||
|
"replacing your document with the server copy.",
|
||||||
|
|
||||||
|
"You can click this message to ignore and contnue."
|
||||||
|
];
|
||||||
|
return toast(false, "box-shadow:0 1em 2em rgba(64,64,64,0.8);font-weight:normal",
|
||||||
|
36, "<p>" + msg.join('</p>\n<p>') + '</p>');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('modpoll eq');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (md_opt.modpoll_freq > 0)
|
||||||
|
this.periodic();
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
var modpoll = new Modpoll();
|
||||||
|
|
||||||
|
|
||||||
|
window.onbeforeunload = function (e) {
|
||||||
|
if ((ebi("save").getAttribute('class') + '').indexOf('disabled') >= 0)
|
||||||
|
return; //nice (todo)
|
||||||
|
|
||||||
|
e.preventDefault(); //ff
|
||||||
|
e.returnValue = ''; //chrome
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
// save handler
|
// save handler
|
||||||
function save(e) {
|
function save(e) {
|
||||||
if (e) e.preventDefault();
|
if (e) e.preventDefault();
|
||||||
var save_btn = document.getElementById("save"),
|
var save_btn = ebi("save"),
|
||||||
save_cls = save_btn.getAttribute('class') + '';
|
save_cls = save_btn.getAttribute('class') + '';
|
||||||
|
|
||||||
if (save_cls.indexOf('disabled') >= 0) {
|
if (save_cls.indexOf('disabled') >= 0) {
|
||||||
toast('font-size:2em;color:#fc6;width:9em;', 'no changes');
|
toast(true, ";font-size:2em;color:#c90", 9, "no changes");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,6 +348,8 @@ function save(e) {
|
|||||||
xhr.onreadystatechange = save_cb;
|
xhr.onreadystatechange = save_cb;
|
||||||
xhr.btn = save_btn;
|
xhr.btn = save_btn;
|
||||||
xhr.txt = txt;
|
xhr.txt = txt;
|
||||||
|
|
||||||
|
modpoll.skip_one = true; // skip one iteration while we save
|
||||||
xhr.send(fd);
|
xhr.send(fd);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,23 +443,44 @@ function savechk_cb() {
|
|||||||
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;',
|
toast(true, ";font-size:6em;font-family:serif;color:#9b4", 4,
|
||||||
'OK✔️<span style="font-size:.2em;color:#999;position:absolute">' + this.ntry + '</span>');
|
'OK✔️<span style="font-size:.2em;color:#999;position:absolute">' + this.ntry + '</span>');
|
||||||
|
|
||||||
|
modpoll.disabled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function toast(style, msg) {
|
function toast(autoclose, style, width, msg) {
|
||||||
var ok = document.createElement('div');
|
var ok = ebi("toast");
|
||||||
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';
|
if (ok)
|
||||||
|
ok.parentNode.removeChild(ok);
|
||||||
|
|
||||||
|
style = "width:" + width + "em;left:calc(50% - " + (width / 2) + "em);" + style;
|
||||||
|
ok = document.createElement('div');
|
||||||
|
ok.setAttribute('id', 'toast');
|
||||||
ok.setAttribute('style', style);
|
ok.setAttribute('style', style);
|
||||||
ok.innerHTML = msg;
|
ok.innerHTML = msg;
|
||||||
var parent = document.getElementById('m');
|
var parent = ebi('m');
|
||||||
document.documentElement.appendChild(ok);
|
document.documentElement.appendChild(ok);
|
||||||
|
|
||||||
|
var hide = function (delay) {
|
||||||
|
delay = delay || 0;
|
||||||
|
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
ok.style.opacity = 0;
|
ok.style.opacity = 0;
|
||||||
}, 500);
|
}, delay);
|
||||||
|
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
|
if (ok.parentNode)
|
||||||
ok.parentNode.removeChild(ok);
|
ok.parentNode.removeChild(ok);
|
||||||
}, 750);
|
}, delay + 250);
|
||||||
|
}
|
||||||
|
|
||||||
|
ok.onclick = function () {
|
||||||
|
hide(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (autoclose)
|
||||||
|
hide(500);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -543,6 +660,10 @@ function md_backspace() {
|
|||||||
if (/^\s*$/.test(left))
|
if (/^\s*$/.test(left))
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
// same if selection
|
||||||
|
if (o0 != dom_src.selectionEnd)
|
||||||
|
return true;
|
||||||
|
|
||||||
// same if line is all-whitespace or non-markup
|
// same if line is all-whitespace or non-markup
|
||||||
var v = m[0].replace(/[^ ]/g, " ");
|
var v = m[0].replace(/[^ ]/g, " ");
|
||||||
if (v === m[0] || v.length !== left.length)
|
if (v === m[0] || v.length !== left.length)
|
||||||
@@ -626,7 +747,8 @@ function fmt_table(e) {
|
|||||||
lpipe = tab[1].indexOf('|') < tab[1].indexOf('-'),
|
lpipe = tab[1].indexOf('|') < tab[1].indexOf('-'),
|
||||||
rpipe = tab[1].lastIndexOf('|') > tab[1].lastIndexOf('-'),
|
rpipe = tab[1].lastIndexOf('|') > tab[1].lastIndexOf('-'),
|
||||||
re_lpipe = lpipe ? /^\s*\|\s*/ : /^\s*/,
|
re_lpipe = lpipe ? /^\s*\|\s*/ : /^\s*/,
|
||||||
re_rpipe = rpipe ? /\s*\|\s*$/ : /\s*$/;
|
re_rpipe = rpipe ? /\s*\|\s*$/ : /\s*$/,
|
||||||
|
ncols;
|
||||||
|
|
||||||
// the second row defines the table,
|
// the second row defines the table,
|
||||||
// need to process that first
|
// need to process that first
|
||||||
@@ -751,8 +873,7 @@ function mark_uni(e) {
|
|||||||
dom_tbox.setAttribute('class', '');
|
dom_tbox.setAttribute('class', '');
|
||||||
|
|
||||||
var txt = dom_src.value,
|
var txt = dom_src.value,
|
||||||
ptn = new RegExp('([^' + js_uni_whitelist + ']+)', 'g');
|
ptn = new RegExp('([^' + js_uni_whitelist + ']+)', 'g'),
|
||||||
|
|
||||||
mod = txt.replace(/\r/g, "").replace(ptn, "\u2588\u2770$1\u2771");
|
mod = txt.replace(/\r/g, "").replace(ptn, "\u2588\u2770$1\u2771");
|
||||||
|
|
||||||
if (txt == mod) {
|
if (txt == mod) {
|
||||||
@@ -789,7 +910,12 @@ function iter_uni(e) {
|
|||||||
// configure whitelist
|
// configure whitelist
|
||||||
function cfg_uni(e) {
|
function cfg_uni(e) {
|
||||||
if (e) e.preventDefault();
|
if (e) e.preventDefault();
|
||||||
esc_uni_whitelist = prompt("unicode whitelist", esc_uni_whitelist);
|
|
||||||
|
var reply = prompt("unicode whitelist", esc_uni_whitelist);
|
||||||
|
if (reply === null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
esc_uni_whitelist = reply;
|
||||||
js_uni_whitelist = eval('\'' + esc_uni_whitelist + '\'');
|
js_uni_whitelist = eval('\'' + esc_uni_whitelist + '\'');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -806,7 +932,7 @@ function cfg_uni(e) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (ev.code == "Escape" || kc == 27) {
|
if (ev.code == "Escape" || kc == 27) {
|
||||||
var d = document.getElementById('helpclose');
|
var d = ebi('helpclose');
|
||||||
if (d)
|
if (d)
|
||||||
d.click();
|
d.click();
|
||||||
}
|
}
|
||||||
@@ -863,22 +989,22 @@ function cfg_uni(e) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
document.onkeydown = keydown;
|
document.onkeydown = keydown;
|
||||||
document.getElementById('save').onclick = save;
|
ebi('save').onclick = save;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
||||||
document.getElementById('tools').onclick = function (e) {
|
ebi('tools').onclick = function (e) {
|
||||||
if (e) e.preventDefault();
|
if (e) e.preventDefault();
|
||||||
var is_open = dom_tbox.getAttribute('class') != 'open';
|
var is_open = dom_tbox.getAttribute('class') != 'open';
|
||||||
dom_tbox.setAttribute('class', is_open ? 'open' : '');
|
dom_tbox.setAttribute('class', is_open ? 'open' : '');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
document.getElementById('help').onclick = function (e) {
|
ebi('help').onclick = function (e) {
|
||||||
if (e) e.preventDefault();
|
if (e) e.preventDefault();
|
||||||
dom_tbox.setAttribute('class', '');
|
dom_tbox.setAttribute('class', '');
|
||||||
|
|
||||||
var dom = document.getElementById('helpbox');
|
var dom = ebi('helpbox');
|
||||||
var dtxt = dom.getElementsByTagName('textarea');
|
var dtxt = dom.getElementsByTagName('textarea');
|
||||||
if (dtxt.length > 0) {
|
if (dtxt.length > 0) {
|
||||||
convert_markdown(dtxt[0].value, dom);
|
convert_markdown(dtxt[0].value, dom);
|
||||||
@@ -886,16 +1012,16 @@ document.getElementById('help').onclick = function (e) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dom.style.display = 'block';
|
dom.style.display = 'block';
|
||||||
document.getElementById('helpclose').onclick = function () {
|
ebi('helpclose').onclick = function () {
|
||||||
dom.style.display = 'none';
|
dom.style.display = 'none';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
document.getElementById('fmt_table').onclick = fmt_table;
|
ebi('fmt_table').onclick = fmt_table;
|
||||||
document.getElementById('mark_uni').onclick = mark_uni;
|
ebi('mark_uni').onclick = mark_uni;
|
||||||
document.getElementById('iter_uni').onclick = iter_uni;
|
ebi('iter_uni').onclick = iter_uni;
|
||||||
document.getElementById('cfg_uni').onclick = cfg_uni;
|
ebi('cfg_uni').onclick = cfg_uni;
|
||||||
|
|
||||||
|
|
||||||
// blame steen
|
// blame steen
|
||||||
@@ -1009,7 +1135,6 @@ action_stack = (function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
push: push,
|
|
||||||
undo: undo,
|
undo: undo,
|
||||||
redo: redo,
|
redo: redo,
|
||||||
push: schedule_push,
|
push: schedule_push,
|
||||||
@@ -1019,7 +1144,7 @@ action_stack = (function () {
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
/*
|
/*
|
||||||
document.getElementById('help').onclick = function () {
|
ebi('help').onclick = function () {
|
||||||
var c1 = getComputedStyle(dom_src).cssText.split(';');
|
var c1 = getComputedStyle(dom_src).cssText.split(';');
|
||||||
var c2 = getComputedStyle(dom_ref).cssText.split(';');
|
var c2 = getComputedStyle(dom_ref).cssText.split(';');
|
||||||
var max = Math.min(c1.length, c2.length);
|
var max = Math.min(c1.length, c2.length);
|
||||||
|
|||||||
@@ -25,7 +25,8 @@
|
|||||||
var last_modified = {{ lastmod }};
|
var last_modified = {{ lastmod }};
|
||||||
var md_opt = {
|
var md_opt = {
|
||||||
link_md_as_html: false,
|
link_md_as_html: false,
|
||||||
allow_plugins: {{ md_plug }}
|
allow_plugins: {{ md_plug }},
|
||||||
|
modpoll_freq: {{ md_chk_rate }}
|
||||||
};
|
};
|
||||||
|
|
||||||
var lightswitch = (function () {
|
var lightswitch = (function () {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
var dom_wrap = document.getElementById('mw');
|
var dom_wrap = ebi('mw');
|
||||||
var dom_nav = document.getElementById('mn');
|
var dom_nav = ebi('mn');
|
||||||
var dom_doc = document.getElementById('m');
|
var dom_doc = ebi('m');
|
||||||
var dom_md = document.getElementById('mt');
|
var dom_md = ebi('mt');
|
||||||
|
|
||||||
(function () {
|
(function () {
|
||||||
var n = document.location + '';
|
var n = document.location + '';
|
||||||
@@ -65,7 +65,7 @@ var mde = (function () {
|
|||||||
mde.codemirror.on("change", function () {
|
mde.codemirror.on("change", function () {
|
||||||
md_changed(mde);
|
md_changed(mde);
|
||||||
});
|
});
|
||||||
var loader = document.getElementById('ml');
|
var loader = ebi('ml');
|
||||||
loader.parentNode.removeChild(loader);
|
loader.parentNode.removeChild(loader);
|
||||||
return mde;
|
return mde;
|
||||||
})();
|
})();
|
||||||
@@ -215,7 +215,7 @@ function save_chk() {
|
|||||||
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');
|
ok.setAttribute('style', 'font-size:6em;font-family:serif;font-weight:bold;color:#cf6;background:#444;border-radius:.3em;padding:.6em 0;position:fixed;top:30%;left:calc(50% - 2em);width:4em;text-align:center;z-index:9001;transition:opacity 0.2s ease-in-out;opacity:1');
|
||||||
ok.innerHTML = 'OK✔️';
|
ok.innerHTML = 'OK✔️';
|
||||||
var parent = document.getElementById('m');
|
var parent = ebi('m');
|
||||||
document.documentElement.appendChild(ok);
|
document.documentElement.appendChild(ok);
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
ok.style.opacity = 0;
|
ok.style.opacity = 0;
|
||||||
|
|||||||
@@ -33,12 +33,12 @@ function goto(dest) {
|
|||||||
for (var a = obj.length - 1; a >= 0; a--)
|
for (var a = obj.length - 1; a >= 0; a--)
|
||||||
obj[a].classList.remove('act');
|
obj[a].classList.remove('act');
|
||||||
|
|
||||||
var obj = document.querySelectorAll('#ops>a');
|
obj = document.querySelectorAll('#ops>a');
|
||||||
for (var a = obj.length - 1; a >= 0; a--)
|
for (var a = obj.length - 1; a >= 0; a--)
|
||||||
obj[a].classList.remove('act');
|
obj[a].classList.remove('act');
|
||||||
|
|
||||||
if (dest) {
|
if (dest) {
|
||||||
document.getElementById('op_' + dest).classList.add('act');
|
ebi('op_' + dest).classList.add('act');
|
||||||
document.querySelector('#ops>a[data-dest=' + dest + ']').classList.add('act');
|
document.querySelector('#ops>a[data-dest=' + dest + ']').classList.add('act');
|
||||||
|
|
||||||
var fn = window['goto_' + dest];
|
var fn = window['goto_' + dest];
|
||||||
@@ -66,7 +66,7 @@ function goto_up2k() {
|
|||||||
if (op !== null && op !== '.')
|
if (op !== null && op !== '.')
|
||||||
goto(op);
|
goto(op);
|
||||||
}
|
}
|
||||||
document.getElementById('ops').style.display = 'block';
|
ebi('ops').style.display = 'block';
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
||||||
@@ -130,7 +130,7 @@ function up2k_init(have_crypto) {
|
|||||||
else
|
else
|
||||||
ebi('u2foot').innerHTML = 'seems like ' + shame + ' so do that if you want more performance';
|
ebi('u2foot').innerHTML = 'seems like ' + shame + ' so do that if you want more performance';
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// show uploader if the user only has write-access
|
// show uploader if the user only has write-access
|
||||||
if (!ebi('files'))
|
if (!ebi('files'))
|
||||||
@@ -262,6 +262,7 @@ function up2k_init(have_crypto) {
|
|||||||
|
|
||||||
more_one_file();
|
more_one_file();
|
||||||
var bad_files = [];
|
var bad_files = [];
|
||||||
|
var good_files = [];
|
||||||
for (var a = 0; a < files.length; a++) {
|
for (var a = 0; a < files.length; a++) {
|
||||||
var fobj = files[a];
|
var fobj = files[a];
|
||||||
if (is_itemlist) {
|
if (is_itemlist) {
|
||||||
@@ -275,9 +276,32 @@ function up2k_init(have_crypto) {
|
|||||||
throw 1;
|
throw 1;
|
||||||
}
|
}
|
||||||
catch (ex) {
|
catch (ex) {
|
||||||
bad_files.push([a, fobj.name]);
|
bad_files.push(fobj.name);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
good_files.push(fobj);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bad_files.length > 0) {
|
||||||
|
var msg = 'These {0} files (of {1} total) were skipped because they are empty:\n'.format(bad_files.length, files.length);
|
||||||
|
for (var a = 0; a < bad_files.length; a++)
|
||||||
|
msg += '-- ' + bad_files[a] + '\n';
|
||||||
|
|
||||||
|
if (files.length - bad_files.length <= 1 && /(android)/i.test(navigator.userAgent))
|
||||||
|
msg += '\nFirefox-Android has a bug which prevents selecting multiple files. Try selecting one file at a time. For more info, see firefox bug 1456557';
|
||||||
|
|
||||||
|
alert(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
var msg = ['upload these ' + good_files.length + ' files?'];
|
||||||
|
for (var a = 0; a < good_files.length; a++)
|
||||||
|
msg.push(good_files[a].name);
|
||||||
|
|
||||||
|
if (!confirm(msg.join('\n')))
|
||||||
|
return;
|
||||||
|
|
||||||
|
for (var a = 0; a < good_files.length; a++) {
|
||||||
|
var fobj = good_files[a];
|
||||||
var now = new Date().getTime();
|
var now = new Date().getTime();
|
||||||
var lmod = fobj.lastModified || now;
|
var lmod = fobj.lastModified || now;
|
||||||
var entry = {
|
var entry = {
|
||||||
@@ -307,17 +331,6 @@ function up2k_init(have_crypto) {
|
|||||||
st.files.push(entry);
|
st.files.push(entry);
|
||||||
st.todo.hash.push(entry);
|
st.todo.hash.push(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bad_files.length > 0) {
|
|
||||||
var msg = 'These {0} files (of {1} total) were skipped because they are empty:\n'.format(bad_files.length, files.length);
|
|
||||||
for (var a = 0; a < bad_files.length; a++)
|
|
||||||
msg += '-- ' + bad_files[a][1] + '\n';
|
|
||||||
|
|
||||||
if (files.length - bad_files.length <= 1 && /(android)/i.test(navigator.userAgent))
|
|
||||||
msg += '\nFirefox-Android has a bug which prevents selecting multiple files. Try selecting one file at a time. For more info, see firefox bug 1456557';
|
|
||||||
|
|
||||||
alert(msg);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
ebi('u2btn').addEventListener('drop', gotfile, false);
|
ebi('u2btn').addEventListener('drop', gotfile, false);
|
||||||
|
|
||||||
@@ -336,16 +349,17 @@ function up2k_init(have_crypto) {
|
|||||||
//
|
//
|
||||||
|
|
||||||
function handshakes_permitted() {
|
function handshakes_permitted() {
|
||||||
return multitask || (
|
var lim = multitask ? 1 : 0;
|
||||||
st.todo.upload.length == 0 &&
|
return lim >=
|
||||||
st.busy.upload.length == 0);
|
st.todo.upload.length +
|
||||||
|
st.busy.upload.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
function hashing_permitted() {
|
function hashing_permitted() {
|
||||||
return multitask || (
|
var lim = multitask ? 1 : 0;
|
||||||
handshakes_permitted() &&
|
return handshakes_permitted() && lim >=
|
||||||
st.todo.handshake.length == 0 &&
|
st.todo.handshake.length +
|
||||||
st.busy.handshake.length == 0);
|
st.busy.handshake.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
var tasker = (function () {
|
var tasker = (function () {
|
||||||
@@ -396,17 +410,6 @@ function up2k_init(have_crypto) {
|
|||||||
/// hashing
|
/// hashing
|
||||||
//
|
//
|
||||||
|
|
||||||
// https://gist.github.com/jonleighton/958841
|
|
||||||
function buf2b64_maybe_fucky(buffer) {
|
|
||||||
var ret = '';
|
|
||||||
var view = new DataView(buffer);
|
|
||||||
for (var i = 0; i < view.byteLength; i++) {
|
|
||||||
ret += String.fromCharCode(view.getUint8(i));
|
|
||||||
}
|
|
||||||
return window.btoa(ret).replace(
|
|
||||||
/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://gist.github.com/jonleighton/958841
|
// https://gist.github.com/jonleighton/958841
|
||||||
function buf2b64(arrayBuffer) {
|
function buf2b64(arrayBuffer) {
|
||||||
var base64 = '';
|
var base64 = '';
|
||||||
@@ -447,20 +450,6 @@ function up2k_init(have_crypto) {
|
|||||||
return base64;
|
return base64;
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest
|
|
||||||
function buf2hex(buffer) {
|
|
||||||
var hexCodes = [];
|
|
||||||
var view = new DataView(buffer);
|
|
||||||
for (var i = 0; i < view.byteLength; i += 4) {
|
|
||||||
var value = view.getUint32(i) // 4 bytes per iter
|
|
||||||
var stringValue = value.toString(16) // doesn't pad
|
|
||||||
var padding = '00000000'
|
|
||||||
var paddedValue = (padding + stringValue).slice(-padding.length)
|
|
||||||
hexCodes.push(paddedValue);
|
|
||||||
}
|
|
||||||
return hexCodes.join("");
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_chunksize(filesize) {
|
function get_chunksize(filesize) {
|
||||||
var chunksize = 1024 * 1024;
|
var chunksize = 1024 * 1024;
|
||||||
var stepsize = 512 * 1024;
|
var stepsize = 512 * 1024;
|
||||||
@@ -692,12 +681,30 @@ function up2k_init(have_crypto) {
|
|||||||
}
|
}
|
||||||
tasker();
|
tasker();
|
||||||
}
|
}
|
||||||
else
|
else {
|
||||||
|
var err = "";
|
||||||
|
var rsp = (xhr.responseText + '');
|
||||||
|
if (rsp.indexOf('partial upload exists') !== -1 ||
|
||||||
|
rsp.indexOf('file already exists') !== -1) {
|
||||||
|
err = rsp;
|
||||||
|
var ofs = err.lastIndexOf(' : ');
|
||||||
|
if (ofs > 0)
|
||||||
|
err = err.slice(0, ofs);
|
||||||
|
}
|
||||||
|
if (err != "") {
|
||||||
|
ebi('f{0}t'.format(t.n)).innerHTML = "ERROR";
|
||||||
|
ebi('f{0}p'.format(t.n)).innerHTML = err;
|
||||||
|
|
||||||
|
st.busy.handshake.splice(st.busy.handshake.indexOf(t), 1);
|
||||||
|
tasker();
|
||||||
|
return;
|
||||||
|
}
|
||||||
alert("server broke (error {0}):\n\"{1}\"\n".format(
|
alert("server broke (error {0}):\n\"{1}\"\n".format(
|
||||||
xhr.status,
|
xhr.status,
|
||||||
(xhr.response && xhr.response.err) ||
|
(xhr.response && xhr.response.err) ||
|
||||||
(xhr.responseText && xhr.responseText) ||
|
(xhr.responseText && xhr.responseText) ||
|
||||||
"no further information"));
|
"no further information"));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
xhr.open('POST', post_url + 'handshake.php', true);
|
xhr.open('POST', post_url + 'handshake.php', true);
|
||||||
xhr.responseType = 'text';
|
xhr.responseType = 'text';
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ function vis_exh(msg, url, lineNo, columnNo, error) {
|
|||||||
document.body.style.fontSize = '0.8em';
|
document.body.style.fontSize = '0.8em';
|
||||||
document.body.style.padding = '0 1em 1em 1em';
|
document.body.style.padding = '0 1em 1em 1em';
|
||||||
hcroak(html.join('\n'));
|
hcroak(html.join('\n'));
|
||||||
};
|
}
|
||||||
|
|
||||||
|
|
||||||
function ebi(id) {
|
function ebi(id) {
|
||||||
|
|||||||
@@ -3,6 +3,14 @@ echo not a script
|
|||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
## delete all partial uploads
|
||||||
|
## (supports linux/macos, probably windows+msys2)
|
||||||
|
|
||||||
|
gzip -d < .hist/up2k.snap | jq -r '.[].tnam' | while IFS= read -r f; do rm -f -- "$f"; done
|
||||||
|
gzip -d < .hist/up2k.snap | jq -r '.[].name' | while IFS= read -r f; do wc -c -- "$f" | grep -qiE '^[^0-9a-z]*0' & rm -f -- "$f"; done
|
||||||
|
|
||||||
|
|
||||||
##
|
##
|
||||||
## create a test payload
|
## create a test payload
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
repacker=1
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
# -- download latest copyparty (source.tgz and sfx),
|
# -- download latest copyparty (source.tgz and sfx),
|
||||||
@@ -19,20 +20,32 @@ set -e
|
|||||||
# -rwxr-xr-x 0 ed ed 183808 Nov 19 00:43 copyparty-extras/sfx-lite/copyparty-sfx.py
|
# -rwxr-xr-x 0 ed ed 183808 Nov 19 00:43 copyparty-extras/sfx-lite/copyparty-sfx.py
|
||||||
|
|
||||||
|
|
||||||
command -v gtar && tar=gtar || tar=tar
|
command -v gtar && tar() { gtar "$@"; }
|
||||||
|
command -v gsed && sed() { gsed "$@"; }
|
||||||
td="$(mktemp -d)"
|
td="$(mktemp -d)"
|
||||||
od="$(pwd)"
|
od="$(pwd)"
|
||||||
cd "$td"
|
cd "$td"
|
||||||
pwd
|
pwd
|
||||||
|
|
||||||
|
|
||||||
# debug: if cache exists, use that instead of bothering github
|
dl_text() {
|
||||||
|
command -v curl && exec curl "$@"
|
||||||
|
exec wget -O- "$@"
|
||||||
|
}
|
||||||
|
dl_files() {
|
||||||
|
command -v curl && exec curl -L --remote-name-all "$@"
|
||||||
|
exec wget "$@"
|
||||||
|
}
|
||||||
|
export -f dl_files
|
||||||
|
|
||||||
|
|
||||||
|
# if cache exists, use that instead of bothering github
|
||||||
cache="$od/.copyparty-repack.cache"
|
cache="$od/.copyparty-repack.cache"
|
||||||
[ -e "$cache" ] &&
|
[ -e "$cache" ] &&
|
||||||
$tar -xvf "$cache" ||
|
tar -xf "$cache" ||
|
||||||
{
|
{
|
||||||
# get download links from github
|
# get download links from github
|
||||||
curl https://api.github.com/repos/9001/copyparty/releases/latest |
|
dl_text https://api.github.com/repos/9001/copyparty/releases/latest |
|
||||||
(
|
(
|
||||||
# prefer jq if available
|
# prefer jq if available
|
||||||
jq -r '.assets[]|select(.name|test("-sfx|tar.gz")).browser_download_url' ||
|
jq -r '.assets[]|select(.name|test("-sfx|tar.gz")).browser_download_url' ||
|
||||||
@@ -41,10 +54,10 @@ cache="$od/.copyparty-repack.cache"
|
|||||||
awk -F\" '/"browser_download_url".*(\.tar\.gz|-sfx\.)/ {print$4}'
|
awk -F\" '/"browser_download_url".*(\.tar\.gz|-sfx\.)/ {print$4}'
|
||||||
) |
|
) |
|
||||||
tee /dev/stderr |
|
tee /dev/stderr |
|
||||||
tr -d '\r' | tr '\n' '\0' | xargs -0 curl -L --remote-name-all
|
tr -d '\r' | tr '\n' '\0' |
|
||||||
|
xargs -0 bash -c 'dl_files "$@"' _
|
||||||
|
|
||||||
# debug: create cache
|
tar -czf "$cache" *
|
||||||
#$tar -czvf "$cache" *
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -57,10 +70,21 @@ mv copyparty-*.tar.gz copyparty-extras/
|
|||||||
|
|
||||||
# unpack the source code
|
# unpack the source code
|
||||||
( cd copyparty-extras/
|
( cd copyparty-extras/
|
||||||
$tar -xvf *.tar.gz
|
tar -xf *.tar.gz
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# use repacker from release if that is newer
|
||||||
|
p_other=copyparty-extras/copyparty-*/scripts/copyparty-repack.sh
|
||||||
|
other=$(awk -F= 'BEGIN{v=-1} NR<10&&/^repacker=/{v=$NF} END{print v}' <$p_other)
|
||||||
|
[ $repacker -lt $other ] &&
|
||||||
|
cat $p_other >"$od/$0" && cd "$od" && rm -rf "$td" && exec "$0" "$@"
|
||||||
|
|
||||||
|
|
||||||
|
# now drop the cache
|
||||||
|
rm -f "$cache"
|
||||||
|
|
||||||
|
|
||||||
# fix permissions
|
# fix permissions
|
||||||
chmod 755 \
|
chmod 755 \
|
||||||
copyparty-extras/sfx-full/* \
|
copyparty-extras/sfx-full/* \
|
||||||
@@ -87,13 +111,15 @@ rm -rf copyparty-{0..9}*.*.*{0..9}
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# and include the repacker itself too
|
# and include the repacker itself too
|
||||||
cp -pv "$od/$0" copyparty-extras/
|
cp -av "$od/$0" copyparty-extras/ ||
|
||||||
|
cp -av "$0" copyparty-extras/ ||
|
||||||
|
true
|
||||||
|
|
||||||
|
|
||||||
# create the bundle
|
# create the bundle
|
||||||
fn=copyparty-$(date +%Y-%m%d-%H%M%S).tgz
|
fn=copyparty-$(date +%Y-%m%d-%H%M%S).tgz
|
||||||
$tar -czvf "$od/$fn" *
|
tar -czvf "$od/$fn" *
|
||||||
cd "$od"
|
cd "$od"
|
||||||
rm -rf "$td"
|
rm -rf "$td"
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import os
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
"""
|
"""
|
||||||
mkdir -p /dev/shm/fusefuzz/{r,v}
|
td=/dev/shm/; [ -e $td ] || td=$HOME; mkdir -p $td/fusefuzz/{r,v}
|
||||||
PYTHONPATH=.. python3 -m copyparty -v /dev/shm/fusefuzz/r::r -i 127.0.0.1
|
PYTHONPATH=.. python3 -m copyparty -v $td/fusefuzz/r::r -i 127.0.0.1
|
||||||
../bin/copyparty-fuse.py /dev/shm/fusefuzz/v http://127.0.0.1:3923/ 2 0
|
../bin/copyparty-fuse.py http://127.0.0.1:3923/ $td/fusefuzz/v -cf 2 -cd 0.5
|
||||||
(d="$PWD"; cd /dev/shm/fusefuzz && "$d"/fusefuzz.py)
|
(d="$PWD"; cd $td/fusefuzz && "$d"/fusefuzz.py)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -18,13 +18,16 @@ echo
|
|||||||
# (the fancy markdown editor)
|
# (the fancy markdown editor)
|
||||||
|
|
||||||
|
|
||||||
command -v gtar >/dev/null &&
|
# port install gnutar findutils gsed coreutils
|
||||||
command -v gfind >/dev/null && {
|
gtar=$(command -v gtar || command -v gnutar) || true
|
||||||
tar() { gtar "$@"; }
|
[ ! -z "$gtar" ] && command -v gfind >/dev/null && {
|
||||||
|
tar() { $gtar "$@"; }
|
||||||
sed() { gsed "$@"; }
|
sed() { gsed "$@"; }
|
||||||
find() { gfind "$@"; }
|
find() { gfind "$@"; }
|
||||||
sort() { gsort "$@"; }
|
sort() { gsort "$@"; }
|
||||||
unexpand() { gunexpand "$@"; }
|
unexpand() { gunexpand "$@"; }
|
||||||
|
command -v grealpath >/dev/null &&
|
||||||
|
realpath() { grealpath "$@"; }
|
||||||
}
|
}
|
||||||
|
|
||||||
[ -e copyparty/__main__.py ] || cd ..
|
[ -e copyparty/__main__.py ] || cd ..
|
||||||
@@ -95,7 +98,7 @@ cd sfx
|
|||||||
}
|
}
|
||||||
|
|
||||||
ver=
|
ver=
|
||||||
command -v git >/dev/null && {
|
git describe --tags >/dev/null 2>/dev/null && {
|
||||||
git_ver="$(git describe --tags)"; # v0.5.5-2-gb164aa0
|
git_ver="$(git describe --tags)"; # v0.5.5-2-gb164aa0
|
||||||
ver="$(printf '%s\n' "$git_ver" | sed -r 's/^v//; s/-g?/./g')";
|
ver="$(printf '%s\n' "$git_ver" | sed -r 's/^v//; s/-g?/./g')";
|
||||||
t_ver=
|
t_ver=
|
||||||
@@ -115,7 +118,7 @@ command -v git >/dev/null && {
|
|||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
dt="$(git log -1 --format=%cd --date=format:'%Y, %m, %d')"
|
dt="$(git log -1 --format=%cd --date=format:'%Y,%m,%d' | sed -E 's/,0?/, /g')"
|
||||||
printf 'git %3s: \033[36m%s\033[0m\n' ver "$ver" dt "$dt"
|
printf 'git %3s: \033[36m%s\033[0m\n' ver "$ver" dt "$dt"
|
||||||
sed -ri '
|
sed -ri '
|
||||||
s/^(VERSION =)(.*)/#\1\2\n\1 ('"$t_ver"')/;
|
s/^(VERSION =)(.*)/#\1\2\n\1 ('"$t_ver"')/;
|
||||||
|
|||||||
@@ -16,15 +16,15 @@ which md5sum 2>/dev/null >/dev/null &&
|
|||||||
|
|
||||||
ver="$1"
|
ver="$1"
|
||||||
|
|
||||||
[[ "x$ver" == x ]] &&
|
[ "x$ver" = x ] &&
|
||||||
{
|
{
|
||||||
echo "need argument 1: version"
|
echo "need argument 1: version"
|
||||||
echo
|
echo
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
[[ -e copyparty/__main__.py ]] || cd ..
|
[ -e copyparty/__main__.py ] || cd ..
|
||||||
[[ -e copyparty/__main__.py ]] ||
|
[ -e copyparty/__main__.py ] ||
|
||||||
{
|
{
|
||||||
echo "run me from within the project root folder"
|
echo "run me from within the project root folder"
|
||||||
echo
|
echo
|
||||||
@@ -35,8 +35,8 @@ mkdir -p dist
|
|||||||
zip_path="$(pwd)/dist/copyparty-$ver.zip"
|
zip_path="$(pwd)/dist/copyparty-$ver.zip"
|
||||||
tgz_path="$(pwd)/dist/copyparty-$ver.tar.gz"
|
tgz_path="$(pwd)/dist/copyparty-$ver.tar.gz"
|
||||||
|
|
||||||
[[ -e "$zip_path" ]] ||
|
[ -e "$zip_path" ] ||
|
||||||
[[ -e "$tgz_path" ]] &&
|
[ -e "$tgz_path" ] &&
|
||||||
{
|
{
|
||||||
echo "found existing archives for this version"
|
echo "found existing archives for this version"
|
||||||
echo " $zip_path"
|
echo " $zip_path"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import os
|
|||||||
import time
|
import time
|
||||||
import json
|
import json
|
||||||
import shutil
|
import shutil
|
||||||
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
import subprocess as sp # nosec
|
import subprocess as sp # nosec
|
||||||
|
|
||||||
@@ -31,9 +32,6 @@ class TestVFS(unittest.TestCase):
|
|||||||
response = self.unfoo(response)
|
response = self.unfoo(response)
|
||||||
self.assertEqual(util.undot(query), response)
|
self.assertEqual(util.undot(query), response)
|
||||||
|
|
||||||
def absify(self, root, names):
|
|
||||||
return ["{}/{}".format(root, x).replace("//", "/") for x in names]
|
|
||||||
|
|
||||||
def ls(self, vfs, vpath, uname):
|
def ls(self, vfs, vpath, uname):
|
||||||
"""helper for resolving and listing a folder"""
|
"""helper for resolving and listing a folder"""
|
||||||
vn, rem = vfs.get(vpath, uname, True, False)
|
vn, rem = vfs.get(vpath, uname, True, False)
|
||||||
@@ -60,23 +58,31 @@ class TestVFS(unittest.TestCase):
|
|||||||
|
|
||||||
if os.path.exists("/Volumes"):
|
if os.path.exists("/Volumes"):
|
||||||
devname, _ = self.chkcmd("hdiutil", "attach", "-nomount", "ram://8192")
|
devname, _ = self.chkcmd("hdiutil", "attach", "-nomount", "ram://8192")
|
||||||
|
devname = devname.strip()
|
||||||
|
print("devname: [{}]".format(devname))
|
||||||
for _ in range(10):
|
for _ in range(10):
|
||||||
try:
|
try:
|
||||||
_, _ = self.chkcmd("diskutil", "eraseVolume", "HFS+", "cptd", devname)
|
_, _ = self.chkcmd(
|
||||||
|
"diskutil", "eraseVolume", "HFS+", "cptd", devname
|
||||||
|
)
|
||||||
return "/Volumes/cptd"
|
return "/Volumes/cptd"
|
||||||
except:
|
except Exception as ex:
|
||||||
print('lol macos')
|
print(repr(ex))
|
||||||
time.sleep(0.25)
|
time.sleep(0.25)
|
||||||
|
|
||||||
raise Exception("ramdisk creation failed")
|
raise Exception("ramdisk creation failed")
|
||||||
|
|
||||||
raise Exception("TODO support windows")
|
ret = os.path.join(tempfile.gettempdir(), "copyparty-test")
|
||||||
|
try:
|
||||||
|
os.mkdir(ret)
|
||||||
|
finally:
|
||||||
|
return ret
|
||||||
|
|
||||||
def log(self, src, msg):
|
def log(self, src, msg):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def test(self):
|
def test(self):
|
||||||
td = self.get_ramdisk() + "/vfs"
|
td = os.path.join(self.get_ramdisk(), "vfs")
|
||||||
try:
|
try:
|
||||||
shutil.rmtree(td)
|
shutil.rmtree(td)
|
||||||
except OSError:
|
except OSError:
|
||||||
@@ -107,7 +113,7 @@ class TestVFS(unittest.TestCase):
|
|||||||
vfs = AuthSrv(Namespace(c=None, a=[], v=["a/ab/::r"]), self.log).vfs
|
vfs = AuthSrv(Namespace(c=None, a=[], v=["a/ab/::r"]), self.log).vfs
|
||||||
self.assertEqual(vfs.nodes, {})
|
self.assertEqual(vfs.nodes, {})
|
||||||
self.assertEqual(vfs.vpath, "")
|
self.assertEqual(vfs.vpath, "")
|
||||||
self.assertEqual(vfs.realpath, td + "/a/ab")
|
self.assertEqual(vfs.realpath, os.path.join(td, "a", "ab"))
|
||||||
self.assertEqual(vfs.uread, ["*"])
|
self.assertEqual(vfs.uread, ["*"])
|
||||||
self.assertEqual(vfs.uwrite, [])
|
self.assertEqual(vfs.uwrite, [])
|
||||||
|
|
||||||
@@ -117,7 +123,7 @@ class TestVFS(unittest.TestCase):
|
|||||||
).vfs
|
).vfs
|
||||||
self.assertEqual(vfs.nodes, {})
|
self.assertEqual(vfs.nodes, {})
|
||||||
self.assertEqual(vfs.vpath, "")
|
self.assertEqual(vfs.vpath, "")
|
||||||
self.assertEqual(vfs.realpath, td + "/a/aa")
|
self.assertEqual(vfs.realpath, os.path.join(td, "a", "aa"))
|
||||||
self.assertEqual(vfs.uread, ["*"])
|
self.assertEqual(vfs.uread, ["*"])
|
||||||
self.assertEqual(vfs.uwrite, [])
|
self.assertEqual(vfs.uwrite, [])
|
||||||
|
|
||||||
@@ -146,42 +152,63 @@ class TestVFS(unittest.TestCase):
|
|||||||
n = n.nodes["acb"]
|
n = n.nodes["acb"]
|
||||||
self.assertEqual(n.nodes, {})
|
self.assertEqual(n.nodes, {})
|
||||||
self.assertEqual(n.vpath, "a/ac/acb")
|
self.assertEqual(n.vpath, "a/ac/acb")
|
||||||
self.assertEqual(n.realpath, td + "/a/ac/acb")
|
self.assertEqual(n.realpath, os.path.join(td, "a", "ac", "acb"))
|
||||||
self.assertEqual(n.uread, ["k"])
|
self.assertEqual(n.uread, ["k"])
|
||||||
self.assertEqual(n.uwrite, ["*", "k"])
|
self.assertEqual(n.uwrite, ["*", "k"])
|
||||||
|
|
||||||
|
# something funky about the windows path normalization,
|
||||||
|
# doesn't really matter but makes the test messy, TODO?
|
||||||
|
|
||||||
fsdir, real, virt = self.ls(vfs, "/", "*")
|
fsdir, real, virt = self.ls(vfs, "/", "*")
|
||||||
self.assertEqual(fsdir, td)
|
self.assertEqual(fsdir, td)
|
||||||
self.assertEqual(real, ["b", "c"])
|
self.assertEqual(real, ["b", "c"])
|
||||||
self.assertEqual(list(virt), ["a"])
|
self.assertEqual(list(virt), ["a"])
|
||||||
|
|
||||||
fsdir, real, virt = self.ls(vfs, "a", "*")
|
fsdir, real, virt = self.ls(vfs, "a", "*")
|
||||||
self.assertEqual(fsdir, td + "/a")
|
self.assertEqual(fsdir, os.path.join(td, "a"))
|
||||||
self.assertEqual(real, ["aa", "ab"])
|
self.assertEqual(real, ["aa", "ab"])
|
||||||
self.assertEqual(list(virt), ["ac"])
|
self.assertEqual(list(virt), ["ac"])
|
||||||
|
|
||||||
fsdir, real, virt = self.ls(vfs, "a/ab", "*")
|
fsdir, real, virt = self.ls(vfs, "a/ab", "*")
|
||||||
self.assertEqual(fsdir, td + "/a/ab")
|
self.assertEqual(fsdir, os.path.join(td, "a", "ab"))
|
||||||
self.assertEqual(real, ["aba", "abb", "abc"])
|
self.assertEqual(real, ["aba", "abb", "abc"])
|
||||||
self.assertEqual(list(virt), [])
|
self.assertEqual(list(virt), [])
|
||||||
|
|
||||||
fsdir, real, virt = self.ls(vfs, "a/ac", "*")
|
fsdir, real, virt = self.ls(vfs, "a/ac", "*")
|
||||||
self.assertEqual(fsdir, td + "/a/ac")
|
self.assertEqual(fsdir, os.path.join(td, "a", "ac"))
|
||||||
self.assertEqual(real, ["aca", "acc"])
|
self.assertEqual(real, ["aca", "acc"])
|
||||||
self.assertEqual(list(virt), [])
|
self.assertEqual(list(virt), [])
|
||||||
|
|
||||||
fsdir, real, virt = self.ls(vfs, "a/ac", "k")
|
fsdir, real, virt = self.ls(vfs, "a/ac", "k")
|
||||||
self.assertEqual(fsdir, td + "/a/ac")
|
self.assertEqual(fsdir, os.path.join(td, "a", "ac"))
|
||||||
self.assertEqual(real, ["aca", "acc"])
|
self.assertEqual(real, ["aca", "acc"])
|
||||||
self.assertEqual(list(virt), ["acb"])
|
self.assertEqual(list(virt), ["acb"])
|
||||||
|
|
||||||
self.assertRaises(util.Pebkac, vfs.get, "a/ac/acb", "*", True, False)
|
self.assertRaises(util.Pebkac, vfs.get, "a/ac/acb", "*", True, False)
|
||||||
|
|
||||||
fsdir, real, virt = self.ls(vfs, "a/ac/acb", "k")
|
fsdir, real, virt = self.ls(vfs, "a/ac/acb", "k")
|
||||||
self.assertEqual(fsdir, td + "/a/ac/acb")
|
self.assertEqual(fsdir, os.path.join(td, "a", "ac", "acb"))
|
||||||
self.assertEqual(real, ["acba", "acbb", "acbc"])
|
self.assertEqual(real, ["acba", "acbb", "acbc"])
|
||||||
self.assertEqual(list(virt), [])
|
self.assertEqual(list(virt), [])
|
||||||
|
|
||||||
|
# admin-only rootfs with all-read-only subfolder
|
||||||
|
vfs = AuthSrv(Namespace(c=None, a=["k:k"], v=[".::ak", "a:a:r"]), self.log,).vfs
|
||||||
|
self.assertEqual(len(vfs.nodes), 1)
|
||||||
|
self.assertEqual(vfs.vpath, "")
|
||||||
|
self.assertEqual(vfs.realpath, td)
|
||||||
|
self.assertEqual(vfs.uread, ["k"])
|
||||||
|
self.assertEqual(vfs.uwrite, ["k"])
|
||||||
|
n = vfs.nodes["a"]
|
||||||
|
self.assertEqual(len(vfs.nodes), 1)
|
||||||
|
self.assertEqual(n.vpath, "a")
|
||||||
|
self.assertEqual(n.realpath, os.path.join(td, "a"))
|
||||||
|
self.assertEqual(n.uread, ["*"])
|
||||||
|
self.assertEqual(n.uwrite, [])
|
||||||
|
self.assertEqual(vfs.can_access("/", "*"), [False, False])
|
||||||
|
self.assertEqual(vfs.can_access("/", "k"), [True, True])
|
||||||
|
self.assertEqual(vfs.can_access("/a", "*"), [True, False])
|
||||||
|
self.assertEqual(vfs.can_access("/a", "k"), [True, False])
|
||||||
|
|
||||||
# breadth-first construction
|
# breadth-first construction
|
||||||
vfs = AuthSrv(
|
vfs = AuthSrv(
|
||||||
Namespace(
|
Namespace(
|
||||||
@@ -215,20 +242,20 @@ class TestVFS(unittest.TestCase):
|
|||||||
self.assertEqual(list(v1), ["a"])
|
self.assertEqual(list(v1), ["a"])
|
||||||
|
|
||||||
fsp, r1, v1 = self.ls(vfs, "a", "*")
|
fsp, r1, v1 = self.ls(vfs, "a", "*")
|
||||||
self.assertEqual(fsp, td + "/a")
|
self.assertEqual(fsp, os.path.join(td, "a"))
|
||||||
self.assertEqual(r1, ["aa", "ab"])
|
self.assertEqual(r1, ["aa", "ab"])
|
||||||
self.assertEqual(list(v1), ["ac"])
|
self.assertEqual(list(v1), ["ac"])
|
||||||
|
|
||||||
fsp1, r1, v1 = self.ls(vfs, "a/ac", "*")
|
fsp1, r1, v1 = self.ls(vfs, "a/ac", "*")
|
||||||
fsp2, r2, v2 = self.ls(vfs, "b", "*")
|
fsp2, r2, v2 = self.ls(vfs, "b", "*")
|
||||||
self.assertEqual(fsp1, td + "/b")
|
self.assertEqual(fsp1, os.path.join(td, "b"))
|
||||||
self.assertEqual(fsp2, td + "/b")
|
self.assertEqual(fsp2, os.path.join(td, "b"))
|
||||||
self.assertEqual(r1, ["ba", "bb", "bc"])
|
self.assertEqual(r1, ["ba", "bb", "bc"])
|
||||||
self.assertEqual(r1, r2)
|
self.assertEqual(r1, r2)
|
||||||
self.assertEqual(list(v1), list(v2))
|
self.assertEqual(list(v1), list(v2))
|
||||||
|
|
||||||
# config file parser
|
# config file parser
|
||||||
cfg_path = self.get_ramdisk() + "/test.cfg"
|
cfg_path = os.path.join(self.get_ramdisk(), "test.cfg")
|
||||||
with open(cfg_path, "wb") as f:
|
with open(cfg_path, "wb") as f:
|
||||||
f.write(
|
f.write(
|
||||||
dedent(
|
dedent(
|
||||||
@@ -256,10 +283,11 @@ class TestVFS(unittest.TestCase):
|
|||||||
self.assertEqual(len(n.nodes), 1)
|
self.assertEqual(len(n.nodes), 1)
|
||||||
n = n.nodes["dst"]
|
n = n.nodes["dst"]
|
||||||
self.assertEqual(n.vpath, "dst")
|
self.assertEqual(n.vpath, "dst")
|
||||||
self.assertEqual(n.realpath, td + "/src")
|
self.assertEqual(n.realpath, os.path.join(td, "src"))
|
||||||
self.assertEqual(n.uread, ["a", "asd"])
|
self.assertEqual(n.uread, ["a", "asd"])
|
||||||
self.assertEqual(n.uwrite, ["asd"])
|
self.assertEqual(n.uwrite, ["asd"])
|
||||||
self.assertEqual(len(n.nodes), 0)
|
self.assertEqual(len(n.nodes), 0)
|
||||||
|
|
||||||
|
os.chdir(tempfile.gettempdir())
|
||||||
shutil.rmtree(td)
|
shutil.rmtree(td)
|
||||||
os.unlink(cfg_path)
|
os.unlink(cfg_path)
|
||||||
|
|||||||
Reference in New Issue
Block a user