mirror of
				https://github.com/9001/copyparty.git
				synced 2025-10-31 03:53:31 +00:00 
			
		
		
		
	Compare commits
	
		
			38 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | c3e4d65b80 | ||
|  | 27a03510c5 | ||
|  | ed7727f7cb | ||
|  | 127ec10c0d | ||
|  | 5a9c0ad225 | ||
|  | 7e8daf650e | ||
|  | 0cf737b4ce | ||
|  | 74635e0113 | ||
|  | e5c4f49901 | ||
|  | e4654ee7f1 | ||
|  | e5d05c05ed | ||
|  | 73c4f99687 | ||
|  | 28c12ef3bf | ||
|  | eed82dbb54 | ||
|  | 2c4b4ab928 | ||
|  | 505a8fc6f6 | ||
|  | e4801d9b06 | ||
|  | 04f1b2cf3a | ||
|  | c06d928bb5 | ||
|  | ab09927e7b | ||
|  | 779437db67 | ||
|  | 28cbdb652e | ||
|  | 2b2415a7d8 | ||
|  | 746a8208aa | ||
|  | a2a041a98a | ||
|  | 10b436e449 | ||
|  | 4d62b34786 | ||
|  | 0546210687 | ||
|  | f8c11faada | ||
|  | 16d6e9be1f | ||
|  | aff8185f2e | ||
|  | 217d15fe81 | ||
|  | 171e93c201 | ||
|  | acc1d2e9e3 | ||
|  | 49c2f37154 | ||
|  | 69e54497aa | ||
|  | 9aa1885669 | ||
|  | 4418508513 | 
							
								
								
									
										10
									
								
								.vscode/launch.py
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								.vscode/launch.py
									
									
									
									
										vendored
									
									
								
							| @@ -5,6 +5,7 @@ | ||||
|  | ||||
| import os | ||||
| import sys | ||||
| import shlex | ||||
|  | ||||
| sys.path.insert(0, os.getcwd()) | ||||
|  | ||||
| @@ -16,9 +17,16 @@ with open(".vscode/launch.json", "r") as f: | ||||
|  | ||||
| oj = jstyleson.loads(tj) | ||||
| argv = oj["configurations"][0]["args"] | ||||
|  | ||||
| try: | ||||
|     sargv = " ".join([shlex.quote(x) for x in argv]) | ||||
|     print(sys.executable + " -m copyparty " + sargv + "\n") | ||||
| except: | ||||
|     pass | ||||
|  | ||||
| argv = [os.path.expanduser(x) if x.startswith("~") else x for x in argv] | ||||
| try: | ||||
|     copyparty(argv) | ||||
|     copyparty(["a"] + argv) | ||||
| except SystemExit as ex: | ||||
|     if ex.code: | ||||
|         raise | ||||
|   | ||||
| @@ -101,6 +101,11 @@ summary: it works! you can use it! (but technically not even close to beta) | ||||
| * hiding the contents at url `/d1/d2/d3` using `-v :d1/d2/d3:cd2d` has the side-effect of creating databases (for files/tags) inside folders d1 and d2, and those databases take precedence over the main db at the top of the vfs - this means all files in d2 and below will be reindexed unless you already had a vfs entry at or below d2 | ||||
| * probably more, pls let me know | ||||
|  | ||||
| ## not my bugs | ||||
|  | ||||
| * Windows: msys2-python 3.8.6 occasionally throws "RuntimeError: release unlocked lock" when leaving a scoped mutex in up2k | ||||
|   * this is an msys2 bug, the regular windows edition of python is fine | ||||
|  | ||||
|  | ||||
| # usage | ||||
|  | ||||
| @@ -111,6 +116,8 @@ the browser has the following hotkeys | ||||
| * `I/K` prev/next folder | ||||
| * `P` parent folder | ||||
|  | ||||
| you can link a particular timestamp in an audio file by adding it to the URL, such as `&20` / `&20s` / `&1m20` / `&1:20` after the `.../#af-c8960dab` | ||||
|  | ||||
|  | ||||
| ## zip downloads | ||||
|  | ||||
| @@ -339,7 +346,6 @@ in the `scripts` folder: | ||||
|  | ||||
| roughly sorted by priority | ||||
|  | ||||
| * audio link with timestamp | ||||
| * separate sqlite table per tag | ||||
| * audio fingerprinting | ||||
| * readme.md as epilogue | ||||
|   | ||||
| @@ -16,6 +16,8 @@ if platform.system() == "Windows": | ||||
| VT100 = not WINDOWS or WINDOWS >= [10, 0, 14393] | ||||
| # introduced in anniversary update | ||||
|  | ||||
| ANYWIN = WINDOWS or sys.platform in ["msys"] | ||||
|  | ||||
| MACOS = platform.system() == "Darwin" | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -247,6 +247,7 @@ def run_argparse(argv, formatter): | ||||
|     ap.add_argument("--no-zip", action="store_true", help="disable download as zip/tar") | ||||
|     ap.add_argument("--no-sendfile", action="store_true", help="disable sendfile (for debugging)") | ||||
|     ap.add_argument("--no-scandir", action="store_true", help="disable scandir (for debugging)") | ||||
|     ap.add_argument("--sparse", metavar="MiB", type=int, default=4, help="up2k min.size threshold (mswin-only)") | ||||
|     ap.add_argument("--urlform", metavar="MODE", type=str, default="print,get", help="how to handle url-forms") | ||||
|     ap.add_argument("--salt", type=str, default="hunter2", help="up2k file-hash salt") | ||||
|  | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| # coding: utf-8 | ||||
|  | ||||
| VERSION = (0, 10, 8) | ||||
| VERSION = (0, 10, 15) | ||||
| CODENAME = "zip it" | ||||
| BUILD_DT = (2021, 4, 11) | ||||
| BUILD_DT = (2021, 4, 24) | ||||
|  | ||||
| S_VERSION = ".".join(map(str, VERSION)) | ||||
| S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT) | ||||
|   | ||||
| @@ -111,7 +111,27 @@ class VFS(object): | ||||
|         if rem: | ||||
|             rp += "/" + rem | ||||
|  | ||||
|         return fsdec(os.path.realpath(fsenc(rp))) | ||||
|         try: | ||||
|             return fsdec(os.path.realpath(fsenc(rp))) | ||||
|         except: | ||||
|             if not WINDOWS: | ||||
|                 raise | ||||
|  | ||||
|             # cpython bug introduced in 3.8, still exists in 3.9.1; | ||||
|             # some win7sp1 and win10:20H2 boxes cannot realpath a | ||||
|             # networked drive letter such as b"n:" or b"n:\\" | ||||
|             # | ||||
|             # requirements to trigger: | ||||
|             #  * bytestring (not unicode str) | ||||
|             #  * just the drive letter (subfolders are ok) | ||||
|             #  * networked drive (regular disks and vmhgfs are ok) | ||||
|             #  * on an enterprise network (idk, cannot repro with samba) | ||||
|             # | ||||
|             # hits the following exceptions in succession: | ||||
|             #  * access denied at L601: "path = _getfinalpathname(path)" | ||||
|             #  * "cant concat str to bytes" at L621: "return path + tail" | ||||
|             # | ||||
|             return os.path.realpath(rp) | ||||
|  | ||||
|     def ls(self, rem, uname, scandir, lstat=False): | ||||
|         """return user-readable [fsdir,real,virt] items at vpath""" | ||||
|   | ||||
| @@ -13,7 +13,7 @@ import ctypes | ||||
| from datetime import datetime | ||||
| import calendar | ||||
|  | ||||
| from .__init__ import E, PY2, WINDOWS | ||||
| from .__init__ import E, PY2, WINDOWS, ANYWIN | ||||
| from .util import *  # noqa  # pylint: disable=unused-wildcard-import | ||||
| from .szip import StreamZip | ||||
| from .star import StreamTar | ||||
| @@ -182,10 +182,8 @@ class HttpCli(object): | ||||
|         self.out_headers.update(headers) | ||||
|  | ||||
|         # default to utf8 html if no content-type is set | ||||
|         try: | ||||
|             mime = mime or self.out_headers["Content-Type"] | ||||
|         except KeyError: | ||||
|             mime = "text/html; charset=UTF-8" | ||||
|         if not mime: | ||||
|             mime = self.out_headers.get("Content-Type", "text/html; charset=UTF-8") | ||||
|  | ||||
|         self.out_headers["Content-Type"] = mime | ||||
|  | ||||
| @@ -261,12 +259,14 @@ class HttpCli(object): | ||||
|  | ||||
|                 self.absolute_urls = True | ||||
|  | ||||
|         # go home if verboten | ||||
|         self.readable, self.writable = self.conn.auth.vfs.can_access( | ||||
|             self.vpath, self.uname | ||||
|         ) | ||||
|         if not self.readable and not self.writable: | ||||
|             self.log("inaccessible: [{}]".format(self.vpath)) | ||||
|             if self.vpath: | ||||
|                 self.log("inaccessible: [{}]".format(self.vpath)) | ||||
|                 raise Pebkac(404) | ||||
|  | ||||
|             self.uparam = {"h": False} | ||||
|  | ||||
|         if "h" in self.uparam: | ||||
| @@ -534,7 +534,7 @@ class HttpCli(object): | ||||
|             self.log("qj: " + repr(vbody)) | ||||
|             hits = idx.fsearch(vols, body) | ||||
|             msg = repr(hits) | ||||
|             taglist = [] | ||||
|             taglist = {} | ||||
|         else: | ||||
|             # search by query params | ||||
|             self.log("qj: " + repr(body)) | ||||
| @@ -626,7 +626,7 @@ class HttpCli(object): | ||||
|             self.loud_reply(x, status=500) | ||||
|             return False | ||||
|  | ||||
|         if not WINDOWS and num_left == 0: | ||||
|         if not ANYWIN and num_left == 0: | ||||
|             times = (int(time.time()), int(lastmod)) | ||||
|             self.log("no more chunks, setting times {}".format(times)) | ||||
|             try: | ||||
| @@ -680,7 +680,7 @@ class HttpCli(object): | ||||
|                 raise Pebkac(500, "mkdir failed, check the logs") | ||||
|  | ||||
|         vpath = "{}/{}".format(self.vpath, sanitized).lstrip("/") | ||||
|         esc_paths = [quotep(vpath), html_escape(vpath)] | ||||
|         esc_paths = [quotep(vpath), html_escape(vpath, crlf=True)] | ||||
|         html = self.j2( | ||||
|             "msg", | ||||
|             h2='<a href="/{}">go to /{}</a>'.format(*esc_paths), | ||||
| @@ -1181,17 +1181,16 @@ class HttpCli(object): | ||||
|         template = self.j2(tpl) | ||||
|  | ||||
|         st = os.stat(fsenc(fs_path)) | ||||
|         # sz_md = st.st_size | ||||
|         ts_md = st.st_mtime | ||||
|  | ||||
|         st = os.stat(fsenc(html_path)) | ||||
|         ts_html = st.st_mtime | ||||
|  | ||||
|         # TODO dont load into memory ;_; | ||||
|         #   (trivial fix, count the &'s) | ||||
|         with open(fsenc(fs_path), "rb") as f: | ||||
|             md = f.read().replace(b"&", b"&") | ||||
|             sz_md = len(md) | ||||
|         sz_md = 0 | ||||
|         for buf in yieldfile(fs_path): | ||||
|             sz_md += len(buf) | ||||
|             for c, v in [[b"&", 4], [b"<", 3], [b">", 3]]: | ||||
|                 sz_md += (len(buf) - len(buf.replace(c, b""))) * v | ||||
|  | ||||
|         file_ts = max(ts_md, ts_html) | ||||
|         file_lastmod, do_send = self._chk_lastmod(file_ts) | ||||
| @@ -1199,27 +1198,34 @@ class HttpCli(object): | ||||
|         self.out_headers["Cache-Control"] = "no-cache" | ||||
|         status = 200 if do_send else 304 | ||||
|  | ||||
|         boundary = "\roll\tide" | ||||
|         targs = { | ||||
|             "edit": "edit" in self.uparam, | ||||
|             "title": html_escape(self.vpath), | ||||
|             "title": html_escape(self.vpath, crlf=True), | ||||
|             "lastmod": int(ts_md * 1000), | ||||
|             "md_plug": "true" if self.args.emp else "false", | ||||
|             "md_chk_rate": self.args.mcr, | ||||
|             "md": "", | ||||
|             "md": boundary, | ||||
|         } | ||||
|         sz_html = len(template.render(**targs).encode("utf-8")) | ||||
|         self.send_headers(sz_html + sz_md, status) | ||||
|         html = template.render(**targs).encode("utf-8") | ||||
|         html = html.split(boundary.encode("utf-8")) | ||||
|         if len(html) != 2: | ||||
|             raise Exception("boundary appears in " + html_path) | ||||
|  | ||||
|         self.send_headers(sz_md + len(html[0]) + len(html[1]), status) | ||||
|  | ||||
|         logmsg += unicode(status) | ||||
|         if self.mode == "HEAD" or not do_send: | ||||
|             self.log(logmsg) | ||||
|             return True | ||||
|  | ||||
|         # TODO jinja2 can stream this right? | ||||
|         targs["md"] = md.decode("utf-8", "replace") | ||||
|         html = template.render(**targs).encode("utf-8") | ||||
|         try: | ||||
|             self.s.sendall(html) | ||||
|             self.s.sendall(html[0]) | ||||
|             for buf in yieldfile(fs_path): | ||||
|                 self.s.sendall(html_bescape(buf)) | ||||
|  | ||||
|             self.s.sendall(html[1]) | ||||
|  | ||||
|         except: | ||||
|             self.log(logmsg + " \033[31md/c\033[0m") | ||||
|             return False | ||||
| @@ -1300,7 +1306,7 @@ class HttpCli(object): | ||||
|                 else: | ||||
|                     vpath += "/" + node | ||||
|  | ||||
|                 vpnodes.append([quotep(vpath) + "/", html_escape(node)]) | ||||
|                 vpnodes.append([quotep(vpath) + "/", html_escape(node, crlf=True)]) | ||||
|  | ||||
|         vn, rem = self.auth.vfs.get( | ||||
|             self.vpath, self.uname, self.readable, self.writable | ||||
| @@ -1311,6 +1317,77 @@ class HttpCli(object): | ||||
|             # print(abspath) | ||||
|             raise Pebkac(404) | ||||
|  | ||||
|         srv_info = [] | ||||
|  | ||||
|         try: | ||||
|             if not self.args.nih: | ||||
|                 srv_info.append(unicode(socket.gethostname()).split(".")[0]) | ||||
|         except: | ||||
|             self.log("#wow #whoa") | ||||
|  | ||||
|         try: | ||||
|             # some fuses misbehave | ||||
|             if not self.args.nid: | ||||
|                 if WINDOWS: | ||||
|                     bfree = ctypes.c_ulonglong(0) | ||||
|                     ctypes.windll.kernel32.GetDiskFreeSpaceExW( | ||||
|                         ctypes.c_wchar_p(abspath), None, None, ctypes.pointer(bfree) | ||||
|                     ) | ||||
|                     srv_info.append(humansize(bfree.value) + " free") | ||||
|                 else: | ||||
|                     sv = os.statvfs(abspath) | ||||
|                     free = humansize(sv.f_frsize * sv.f_bfree, True) | ||||
|                     total = humansize(sv.f_frsize * sv.f_blocks, True) | ||||
|  | ||||
|                     srv_info.append(free + " free") | ||||
|                     srv_info.append(total) | ||||
|         except: | ||||
|             pass | ||||
|  | ||||
|         srv_info = "</span> /// <span>".join(srv_info) | ||||
|  | ||||
|         perms = [] | ||||
|         if self.readable: | ||||
|             perms.append("read") | ||||
|         if self.writable: | ||||
|             perms.append("write") | ||||
|  | ||||
|         url_suf = self.urlq() | ||||
|         is_ls = "ls" in self.uparam | ||||
|         ts = ""  # "?{}".format(time.time()) | ||||
|  | ||||
|         tpl = "browser" | ||||
|         if "b" in self.uparam: | ||||
|             tpl = "browser2" | ||||
|  | ||||
|         j2a = { | ||||
|             "vdir": quotep(self.vpath), | ||||
|             "vpnodes": vpnodes, | ||||
|             "files": [], | ||||
|             "ts": ts, | ||||
|             "perms": json.dumps(perms), | ||||
|             "taglist": [], | ||||
|             "tag_order": [], | ||||
|             "have_up2k_idx": ("e2d" in vn.flags), | ||||
|             "have_tags_idx": ("e2t" in vn.flags), | ||||
|             "have_zip": (not self.args.no_zip), | ||||
|             "have_b_u": (self.writable and self.uparam.get("b") == "u"), | ||||
|             "url_suf": url_suf, | ||||
|             "logues": ["", ""], | ||||
|             "title": html_escape(self.vpath, crlf=True), | ||||
|             "srv_info": srv_info, | ||||
|         } | ||||
|         if not self.readable: | ||||
|             if is_ls: | ||||
|                 raise Pebkac(403) | ||||
|  | ||||
|             if not os.path.isdir(fsenc(abspath)): | ||||
|                 raise Pebkac(404) | ||||
|  | ||||
|             html = self.j2(tpl, **j2a) | ||||
|             self.reply(html.encode("utf-8", "replace")) | ||||
|             return True | ||||
|  | ||||
|         if not os.path.isdir(fsenc(abspath)): | ||||
|             if abspath.endswith(".md") and "raw" not in self.uparam: | ||||
|                 return self.tx_md(abspath) | ||||
| @@ -1354,15 +1431,11 @@ class HttpCli(object): | ||||
|         if rem == ".hist": | ||||
|             hidden = ["up2k."] | ||||
|  | ||||
|         is_ls = "ls" in self.uparam | ||||
|  | ||||
|         icur = None | ||||
|         if "e2t" in vn.flags: | ||||
|             idx = self.conn.get_u2idx() | ||||
|             icur = idx.get_cur(vn.realpath) | ||||
|  | ||||
|         url_suf = self.urlq() | ||||
|  | ||||
|         dirs = [] | ||||
|         files = [] | ||||
|         for fn in vfs_ls: | ||||
| @@ -1394,7 +1467,7 @@ class HttpCli(object): | ||||
|                     margin = '<a href="{}?zip">zip</a>'.format(quotep(href)) | ||||
|             elif fn in hist: | ||||
|                 margin = '<a href="{}.hist/{}">#{}</a>'.format( | ||||
|                     base, html_escape(hist[fn][2], quote=True), hist[fn][0] | ||||
|                     base, html_escape(hist[fn][2], quote=True, crlf=True), hist[fn][0] | ||||
|                 ) | ||||
|             else: | ||||
|                 margin = "-" | ||||
| @@ -1453,42 +1526,6 @@ class HttpCli(object): | ||||
|             for f in dirs: | ||||
|                 f["tags"] = {} | ||||
|  | ||||
|         srv_info = [] | ||||
|  | ||||
|         try: | ||||
|             if not self.args.nih: | ||||
|                 srv_info.append(unicode(socket.gethostname()).split(".")[0]) | ||||
|         except: | ||||
|             self.log("#wow #whoa") | ||||
|             pass | ||||
|  | ||||
|         try: | ||||
|             # some fuses misbehave | ||||
|             if not self.args.nid: | ||||
|                 if WINDOWS: | ||||
|                     bfree = ctypes.c_ulonglong(0) | ||||
|                     ctypes.windll.kernel32.GetDiskFreeSpaceExW( | ||||
|                         ctypes.c_wchar_p(abspath), None, None, ctypes.pointer(bfree) | ||||
|                     ) | ||||
|                     srv_info.append(humansize(bfree.value) + " free") | ||||
|                 else: | ||||
|                     sv = os.statvfs(abspath) | ||||
|                     free = humansize(sv.f_frsize * sv.f_bfree, True) | ||||
|                     total = humansize(sv.f_frsize * sv.f_blocks, True) | ||||
|  | ||||
|                     srv_info.append(free + " free") | ||||
|                     srv_info.append(total) | ||||
|         except: | ||||
|             pass | ||||
|  | ||||
|         srv_info = "</span> /// <span>".join(srv_info) | ||||
|  | ||||
|         perms = [] | ||||
|         if self.readable: | ||||
|             perms.append("read") | ||||
|         if self.writable: | ||||
|             perms.append("write") | ||||
|  | ||||
|         logues = ["", ""] | ||||
|         for n, fn in enumerate([".prologue.html", ".epilogue.html"]): | ||||
|             fn = os.path.join(abspath, fn) | ||||
| @@ -1510,34 +1547,12 @@ class HttpCli(object): | ||||
|             self.reply(ret.encode("utf-8", "replace"), mime="application/json") | ||||
|             return True | ||||
|  | ||||
|         ts = "" | ||||
|         # ts = "?{}".format(time.time()) | ||||
|         j2a["files"] = dirs + files | ||||
|         j2a["logues"] = logues | ||||
|         j2a["taglist"] = taglist | ||||
|         if "mte" in vn.flags: | ||||
|             j2a["tag_order"] = json.dumps(vn.flags["mte"].split(",")) | ||||
|  | ||||
|         dirs.extend(files) | ||||
|  | ||||
|         tpl = "browser" | ||||
|         if "b" in self.uparam: | ||||
|             tpl = "browser2" | ||||
|  | ||||
|         html = self.j2( | ||||
|             tpl, | ||||
|             vdir=quotep(self.vpath), | ||||
|             vpnodes=vpnodes, | ||||
|             files=dirs, | ||||
|             ts=ts, | ||||
|             perms=json.dumps(perms), | ||||
|             taglist=taglist, | ||||
|             tag_order=json.dumps( | ||||
|                 vn.flags["mte"].split(",") if "mte" in vn.flags else [] | ||||
|             ), | ||||
|             have_up2k_idx=("e2d" in vn.flags), | ||||
|             have_tags_idx=("e2t" in vn.flags), | ||||
|             have_zip=(not self.args.no_zip), | ||||
|             have_b_u=(self.writable and self.uparam.get("b") == "u"), | ||||
|             url_suf=url_suf, | ||||
|             logues=logues, | ||||
|             title=html_escape(self.vpath), | ||||
|             srv_info=srv_info, | ||||
|         ) | ||||
|         html = self.j2(tpl, **j2a) | ||||
|         self.reply(html.encode("utf-8", "replace")) | ||||
|         return True | ||||
|   | ||||
| @@ -16,7 +16,7 @@ import traceback | ||||
| import subprocess as sp | ||||
| from copy import deepcopy | ||||
|  | ||||
| from .__init__ import WINDOWS | ||||
| from .__init__ import WINDOWS, ANYWIN | ||||
| from .util import ( | ||||
|     Pebkac, | ||||
|     Queue, | ||||
| @@ -79,7 +79,7 @@ class Up2k(object): | ||||
|             if self.sqlite_ver < (3, 9): | ||||
|                 self.no_expr_idx = True | ||||
|  | ||||
|         if WINDOWS: | ||||
|         if ANYWIN: | ||||
|             # usually fails to set lastmod too quickly | ||||
|             self.lastmod_q = Queue() | ||||
|             thr = threading.Thread(target=self._lastmodder) | ||||
| @@ -101,17 +101,18 @@ class Up2k(object): | ||||
|             thr.daemon = True | ||||
|             thr.start() | ||||
|  | ||||
|             thr = threading.Thread(target=self._tagger) | ||||
|             thr.daemon = True | ||||
|             thr.start() | ||||
|  | ||||
|             thr = threading.Thread(target=self._hasher) | ||||
|             thr.daemon = True | ||||
|             thr.start() | ||||
|  | ||||
|             thr = threading.Thread(target=self._run_all_mtp) | ||||
|             thr.daemon = True | ||||
|             thr.start() | ||||
|             if self.mtag: | ||||
|                 thr = threading.Thread(target=self._tagger) | ||||
|                 thr.daemon = True | ||||
|                 thr.start() | ||||
|  | ||||
|                 thr = threading.Thread(target=self._run_all_mtp) | ||||
|                 thr.daemon = True | ||||
|                 thr.start() | ||||
|  | ||||
|     def log(self, msg, c=0): | ||||
|         self.log_func("up2k", msg + "\033[K", c) | ||||
| @@ -667,12 +668,6 @@ class Up2k(object): | ||||
|             cur.close() | ||||
|  | ||||
|     def _start_mpool(self): | ||||
|         if WINDOWS and False: | ||||
|             nah = open(os.devnull, "wb") | ||||
|             wmic = "processid={}".format(os.getpid()) | ||||
|             wmic = ["wmic", "process", "where", wmic, "call", "setpriority"] | ||||
|             sp.call(wmic + ["below normal"], stdout=nah, stderr=nah) | ||||
|  | ||||
|         # mp.pool.ThreadPool and concurrent.futures.ThreadPoolExecutor | ||||
|         # both do crazy runahead so lets reinvent another wheel | ||||
|         nw = os.cpu_count() if hasattr(os, "cpu_count") else 4 | ||||
| @@ -697,12 +692,6 @@ class Up2k(object): | ||||
|  | ||||
|         mpool.join() | ||||
|         done = self._flush_mpool(wcur) | ||||
|         if WINDOWS and False: | ||||
|             nah = open(os.devnull, "wb") | ||||
|             wmic = "processid={}".format(os.getpid()) | ||||
|             wmic = ["wmic", "process", "where", wmic, "call", "setpriority"] | ||||
|             sp.call(wmic + ["below normal"], stdout=nah, stderr=nah) | ||||
|  | ||||
|         return done | ||||
|  | ||||
|     def _tag_thr(self, q): | ||||
| @@ -1068,6 +1057,8 @@ class Up2k(object): | ||||
|         with self.mutex: | ||||
|             job = self.registry[ptop].get(wark, None) | ||||
|             if not job: | ||||
|                 known = " ".join([x for x in self.registry[ptop].keys()]) | ||||
|                 self.log("unknown wark [{}], known: {}".format(wark, known)) | ||||
|                 raise Pebkac(400, "unknown wark") | ||||
|  | ||||
|             if chash not in job["need"]: | ||||
| @@ -1107,8 +1098,9 @@ class Up2k(object): | ||||
|  | ||||
|             atomic_move(src, dst) | ||||
|  | ||||
|             if WINDOWS: | ||||
|                 self.lastmod_q.put([dst, (int(time.time()), int(job["lmod"]))]) | ||||
|             if ANYWIN: | ||||
|                 a = [dst, job["size"], (int(time.time()), int(job["lmod"]))] | ||||
|                 self.lastmod_q.put(a) | ||||
|  | ||||
|             # legit api sware 2 me mum | ||||
|             if self.idx_wark( | ||||
| @@ -1209,6 +1201,17 @@ class Up2k(object): | ||||
|         suffix = ".{:.6f}-{}".format(job["t0"], job["addr"]) | ||||
|         with ren_open(tnam, "wb", fdir=pdir, suffix=suffix) as f: | ||||
|             f, job["tnam"] = f["orz"] | ||||
|             if ( | ||||
|                 ANYWIN | ||||
|                 and self.args.sparse | ||||
|                 and self.args.sparse * 1024 * 1024 <= job["size"] | ||||
|             ): | ||||
|                 fp = os.path.join(pdir, job["tnam"]) | ||||
|                 try: | ||||
|                     sp.check_call(["fsutil", "sparse", "setflag", fp]) | ||||
|                 except: | ||||
|                     self.log("could not sparse [{}]".format(fp), 3) | ||||
|  | ||||
|             f.seek(job["size"] - 1) | ||||
|             f.write(b"e") | ||||
|  | ||||
| @@ -1220,13 +1223,19 @@ class Up2k(object): | ||||
|  | ||||
|             # self.log("lmod: got {}".format(len(ready))) | ||||
|             time.sleep(5) | ||||
|             for path, times in ready: | ||||
|             for path, sz, times in ready: | ||||
|                 self.log("lmod: setting times {} on {}".format(times, path)) | ||||
|                 try: | ||||
|                     os.utime(fsenc(path), times) | ||||
|                 except: | ||||
|                     self.log("lmod: failed to utime ({}, {})".format(path, times)) | ||||
|  | ||||
|                 if self.args.sparse and self.args.sparse * 1024 * 1024 <= sz: | ||||
|                     try: | ||||
|                         sp.check_call(["fsutil", "sparse", "setflag", path, "0"]) | ||||
|                     except: | ||||
|                         self.log("could not unsparse [{}]".format(path), 3) | ||||
|  | ||||
|     def _snapshot(self): | ||||
|         persist_interval = 30  # persist unfinished uploads index every 30 sec | ||||
|         discard_interval = 21600  # drop unfinished uploads after 6 hours inactivity | ||||
|   | ||||
| @@ -16,7 +16,7 @@ import mimetypes | ||||
| import contextlib | ||||
| import subprocess as sp  # nosec | ||||
|  | ||||
| from .__init__ import PY2, WINDOWS | ||||
| from .__init__ import PY2, WINDOWS, ANYWIN | ||||
| from .stolen import surrogateescape | ||||
|  | ||||
| FAKE_MP = False | ||||
| @@ -580,8 +580,8 @@ def sanitize_fn(fn, ok=""): | ||||
|     if "/" not in ok: | ||||
|         fn = fn.replace("\\", "/").split("/")[-1] | ||||
|  | ||||
|     if WINDOWS: | ||||
|         for bad, good in [x for x in [ | ||||
|     if ANYWIN: | ||||
|         remap = [ | ||||
|             ["<", "<"], | ||||
|             [">", ">"], | ||||
|             [":", ":"], | ||||
| @@ -591,7 +591,8 @@ def sanitize_fn(fn, ok=""): | ||||
|             ["|", "|"], | ||||
|             ["?", "?"], | ||||
|             ["*", "*"], | ||||
|         ] if x[0] not in ok]: | ||||
|         ] | ||||
|         for bad, good in [x for x in remap if x[0] not in ok]: | ||||
|             fn = fn.replace(bad, good) | ||||
|  | ||||
|         bad = ["con", "prn", "aux", "nul"] | ||||
| @@ -615,17 +616,24 @@ def exclude_dotfiles(filepaths): | ||||
|     return [x for x in filepaths if not x.split("/")[-1].startswith(".")] | ||||
|  | ||||
|  | ||||
| def html_escape(s, quote=False): | ||||
| def html_escape(s, quote=False, crlf=False): | ||||
|     """html.escape but also newlines""" | ||||
|     s = ( | ||||
|         s.replace("&", "&") | ||||
|         .replace("<", "<") | ||||
|         .replace(">", ">") | ||||
|         .replace("\r", "
") | ||||
|         .replace("\n", "
") | ||||
|     ) | ||||
|     s = s.replace("&", "&").replace("<", "<").replace(">", ">") | ||||
|     if quote: | ||||
|         s = s.replace('"', """).replace("'", "'") | ||||
|     if crlf: | ||||
|         s = s.replace("\r", "
").replace("\n", "
") | ||||
|  | ||||
|     return s | ||||
|  | ||||
|  | ||||
| def html_bescape(s, quote=False, crlf=False): | ||||
|     """html.escape but bytestrings""" | ||||
|     s = s.replace(b"&", b"&").replace(b"<", b"<").replace(b">", b">") | ||||
|     if quote: | ||||
|         s = s.replace(b'"', b""").replace(b"'", b"'") | ||||
|     if crlf: | ||||
|         s = s.replace(b"\r", b"
").replace(b"\n", b"
") | ||||
|  | ||||
|     return s | ||||
|  | ||||
|   | ||||
| @@ -68,7 +68,7 @@ a, #files tbody div a:last-child { | ||||
| 	color: #999; | ||||
| 	font-weight: normal; | ||||
| } | ||||
| #files tr+tr:hover { | ||||
| #files tr:hover { | ||||
| 	background: #1c1c1c; | ||||
| } | ||||
| #files thead th { | ||||
| @@ -98,7 +98,7 @@ a, #files tbody div a:last-child { | ||||
| 	max-width: 30em; | ||||
| 	overflow: hidden; | ||||
| } | ||||
| #files tr+tr td { | ||||
| #files tr td { | ||||
| 	border-top: 1px solid #383838; | ||||
| } | ||||
| #files tbody td:nth-child(3) { | ||||
| @@ -284,7 +284,7 @@ a, #files tbody div a:last-child { | ||||
| 	line-height: 1em; | ||||
| } | ||||
| #wtoggle.sel { | ||||
| 	width: 6em; | ||||
| 	width: 6.4em; | ||||
| } | ||||
| #wtoggle.sel #wzip { | ||||
| 	display: inline-block; | ||||
| @@ -685,3 +685,186 @@ input[type="checkbox"]:checked+label { | ||||
| 	font-family: monospace, monospace; | ||||
| 	line-height: 2em; | ||||
| } | ||||
| #pvol, | ||||
| #barbuf, | ||||
| #barpos, | ||||
| #u2conf label { | ||||
| 	-webkit-user-select: none; | ||||
| 	-moz-user-select: none; | ||||
| 	-ms-user-select: none; | ||||
| 	user-select: none; | ||||
| } | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| html.light { | ||||
| 	color: #333; | ||||
| 	background: #eee; | ||||
| 	text-shadow: none; | ||||
| } | ||||
| html.light #ops, | ||||
| html.light .opbox, | ||||
| html.light #srch_form { | ||||
| 	background: #f7f7f7; | ||||
| 	box-shadow: 0 0 .3em #ddd; | ||||
| 	border-color: #f7f7f7; | ||||
| } | ||||
| html.light #ops a.act { | ||||
| 	box-shadow: 0 .2em .2em #ccc; | ||||
| 	background: #fff; | ||||
| 	border-color: #07a; | ||||
| 	padding-top: .4em; | ||||
| } | ||||
| html.light #op_cfg h3 { | ||||
| 	border-color: #ccc; | ||||
| } | ||||
| html.light .tglbtn, | ||||
| html.light #tree > a + a { | ||||
| 	color: #666; | ||||
| 	background: #ddd; | ||||
| 	box-shadow: none; | ||||
| } | ||||
| html.light .tglbtn:hover, | ||||
| html.light #tree > a + a:hover { | ||||
| 	background: #caf; | ||||
| } | ||||
| html.light .tglbtn.on, | ||||
| html.light #tree > a + a.on { | ||||
| 	background: #4a0; | ||||
| 	color: #fff; | ||||
| } | ||||
| html.light #srv_info { | ||||
| 	color: #c83; | ||||
| 	text-shadow: 1px 1px 0 #fff; | ||||
| } | ||||
| html.light #srv_info span { | ||||
| 	color: #000; | ||||
| } | ||||
| html.light #treeul a+a { | ||||
| 	background: inherit; | ||||
| 	color: #06a; | ||||
| } | ||||
| html.light #treeul a.hl { | ||||
| 	background: #07a; | ||||
| 	color: #fff; | ||||
| } | ||||
| html.light #tree li { | ||||
| 	border-color: #ddd #fff #f7f7f7 #fff; | ||||
| } | ||||
| html.light #tree ul { | ||||
| 	border-color: #ccc; | ||||
| } | ||||
| html.light a, | ||||
| html.light #ops a, | ||||
| html.light #files tbody div a:last-child { | ||||
| 	color: #06a; | ||||
| } | ||||
| html.light #files tbody { | ||||
| 	background: #f7f7f7; | ||||
| } | ||||
| html.light #files { | ||||
| 	box-shadow: 0 0 .3em #ccc; | ||||
| } | ||||
| html.light #files thead th { | ||||
| 	background: #eee; | ||||
| } | ||||
| html.light #files tr td { | ||||
| 	border-top: 1px solid #ddd; | ||||
| } | ||||
| html.light #files td { | ||||
| 	border-bottom: 1px solid #f7f7f7; | ||||
| } | ||||
| html.light #files tbody tr:last-child td { | ||||
| 	border-bottom: .2em solid #ccc; | ||||
| } | ||||
| html.light #files td:nth-child(2n) { | ||||
| 	color: #d38; | ||||
| } | ||||
| html.light #files tr:hover td { | ||||
| 	background: #fff; | ||||
| } | ||||
| html.light #files tbody a.play { | ||||
| 	color: #c0f; | ||||
| } | ||||
| html.light tr.play td { | ||||
| 	background: #fc5; | ||||
| } | ||||
| html.light tr.play a { | ||||
| 	color: #406; | ||||
| } | ||||
| html.light #files th:hover .cfg, | ||||
| html.light #files th.min .cfg { | ||||
| 	background: #ccc; | ||||
| } | ||||
| html.light #files > thead > tr > th.min span { | ||||
| 	background: linear-gradient(90deg, rgba(204,204,204,0), rgba(204,204,204,0.5) 70%, #ccc); | ||||
| } | ||||
| html.light #blocked { | ||||
| 	background: #eee; | ||||
| } | ||||
| html.light #blk_play a, | ||||
| html.light #blk_abrt a { | ||||
| 	background: #fff; | ||||
| 	box-shadow: 0 .2em .4em #ddd; | ||||
| } | ||||
| html.light #widget a { | ||||
| 	color: #fc5; | ||||
| } | ||||
| html.light #files tr.sel:hover td { | ||||
| 	background: #c37; | ||||
| } | ||||
| html.light #files tr.sel td { | ||||
| 	color: #fff; | ||||
| } | ||||
| html.light #files tr.sel a { | ||||
| 	color: #fff; | ||||
| } | ||||
| html.light input[type="checkbox"] + label { | ||||
| 	color: #333; | ||||
| } | ||||
| html.light .opview input[type="text"] { | ||||
| 	background: #fff; | ||||
| 	color: #333; | ||||
| 	box-shadow: 0 0 2px #888; | ||||
| 	border-color: #38d; | ||||
| } | ||||
| html.light #ops:hover #opdesc { | ||||
| 	background: #fff; | ||||
| 	box-shadow: 0 .3em 1em #ccc; | ||||
| } | ||||
| html.light #opdesc code { | ||||
| 	background: #060; | ||||
| 	color: #fff; | ||||
| } | ||||
| html.light #u2tab a>span, | ||||
| html.light #files td div span { | ||||
| 	color: #000; | ||||
| } | ||||
| html.light #path { | ||||
| 	background: #f7f7f7; | ||||
| 	text-shadow: none; | ||||
| 	box-shadow: 0 0 .3em #bbb; | ||||
| } | ||||
| html.light #path a { | ||||
| 	color: #333; | ||||
| } | ||||
| html.light #path a:not(:last-child)::after { | ||||
| 	border-color: #ccc; | ||||
| 	background: none; | ||||
| 	border-width: .1em .1em 0 0; | ||||
| 	margin: -.2em .3em -.2em -.3em; | ||||
| } | ||||
| html.light #path a:hover { | ||||
| 	background: none; | ||||
| 	color: #60a; | ||||
| } | ||||
| html.light #files tbody div a { | ||||
| 	color: #d38; | ||||
| } | ||||
| html.light #files a:hover, | ||||
| html.light #files tr.sel a:hover { | ||||
| 	color: #000; | ||||
| 	background: #fff; | ||||
| } | ||||
| @@ -13,8 +13,8 @@ | ||||
| <body> | ||||
|     <div id="ops"> | ||||
|         <a href="#" data-dest="" data-desc="close submenu">---</a> | ||||
|         <a href="#" data-perm="read" data-dest="search" data-desc="search for files by attributes, path/name, music tags, or any combination of those.<br /><br /><code>foo bar</code> = must contain both foo and bar,<br /><code>foo -bar</code> = must contain foo but not bar,<br /><code>^yana .opus$</code> = must start with yana and have the opus extension">🔎</a> | ||||
|         {%- if have_up2k_idx %} | ||||
|         <a href="#" data-perm="read" data-dest="search" data-desc="search for files by attributes, path/name, music tags, or any combination of those.<br /><br /><code>foo bar</code> = must contain both foo and bar,<br /><code>foo -bar</code> = must contain foo but not bar,<br /><code>^yana .opus$</code> = must start with yana and have the opus extension">🔎</a> | ||||
|         <a href="#" data-dest="up2k" data-desc="up2k: upload files (if you have write-access) or toggle into the search-mode and drag files onto the search button to see if they exist somewhere on the server">🚀</a> | ||||
|         {%- else %} | ||||
|         <a href="#" data-perm="write" data-dest="up2k" data-desc="up2k: upload files with resume support (close your browser and drop the same files in later)">🚀</a> | ||||
| @@ -39,14 +39,17 @@ | ||||
|     {%- include 'upload.html' %} | ||||
|  | ||||
|     <div id="op_cfg" class="opview opbox"> | ||||
|         <h3>key notation</h3> | ||||
|         <div id="key_notation"></div> | ||||
|         <h3>switches</h3> | ||||
|         <div> | ||||
|             <a id="tooltips" class="tglbtn" href="#">tooltips</a> | ||||
|             <a id="lightmode" class="tglbtn" href="#">lightmode</a> | ||||
|         </div> | ||||
|         {%- if have_zip %} | ||||
|         <h3>folder download</h3> | ||||
|         <div id="arc_fmt"></div> | ||||
|         {%- endif %} | ||||
|         <h3>tooltips</h3> | ||||
|         <div><a id="tooltips" class="tglbtn" href="#">enable</a></div> | ||||
|         <h3>key notation</h3> | ||||
|         <div id="key_notation"></div> | ||||
|     </div> | ||||
|      | ||||
|     <h1 id="path"> | ||||
| @@ -117,8 +120,8 @@ | ||||
|                 <a href="#" id="selall">sel.<br />all</a> | ||||
|                 <a href="#" id="selinv">sel.<br />inv.</a> | ||||
|                 <a href="#" id="selzip">zip</a> | ||||
|             </span> | ||||
|             <a href="#" id="wtico">♫</a> | ||||
|             </span><a | ||||
|                 href="#" id="wtico">♫</a> | ||||
|         </div> | ||||
|         <div id="widgeti"> | ||||
|             <div id="pctl"><a href="#" id="bprev">⏮</a><a href="#" id="bplay">▶</a><a href="#" id="bnext">⏭</a></div> | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -50,6 +50,9 @@ pre code:last-child { | ||||
| pre code::before { | ||||
| 	content: counter(precode); | ||||
| 	-webkit-user-select: none; | ||||
| 	-moz-user-select: none; | ||||
| 	-ms-user-select: none; | ||||
| 	user-select: none; | ||||
| 	display: inline-block; | ||||
| 	text-align: right; | ||||
| 	font-size: .75em; | ||||
|   | ||||
| @@ -138,10 +138,10 @@ var md_opt = { | ||||
|         document.documentElement.setAttribute("class", dark ? "dark" : ""); | ||||
|         btn.innerHTML = "go " + (dark ? "light" : "dark"); | ||||
|         if (window.localStorage) | ||||
|             localStorage.setItem('darkmode', dark ? 1 : 0); | ||||
|             localStorage.setItem('lightmode', dark ? 0 : 1); | ||||
|     }; | ||||
|     btn.onclick = toggle; | ||||
|     if (window.localStorage && localStorage.getItem('darkmode') == 1) | ||||
|     if (window.localStorage && localStorage.getItem('lightmode') != 1) | ||||
| 		toggle(); | ||||
| })(); | ||||
|  | ||||
|   | ||||
| @@ -46,7 +46,7 @@ function statify(obj) { | ||||
|     var ua = navigator.userAgent; | ||||
|     if (ua.indexOf(') Gecko/') !== -1 && /Linux| Mac /.exec(ua)) { | ||||
|         // necessary on ff-68.7 at least | ||||
|         var s = document.createElement('style'); | ||||
|         var s = mknod('style'); | ||||
|         s.innerHTML = '@page { margin: .5in .6in .8in .6in; }'; | ||||
|         console.log(s.innerHTML); | ||||
|         document.head.appendChild(s); | ||||
| @@ -175,12 +175,12 @@ function md_plug_err(ex, js) { | ||||
|         msg = "Line " + ln + ", " + msg; | ||||
|         var lns = js.split('\n'); | ||||
|         if (ln < lns.length) { | ||||
|             o = document.createElement('span'); | ||||
|             o = mknod('span'); | ||||
|             o.style.cssText = 'color:#ac2;font-size:.9em;font-family:scp;display:block'; | ||||
|             o.textContent = lns[ln - 1]; | ||||
|         } | ||||
|     } | ||||
|     errbox = document.createElement('div'); | ||||
|     errbox = mknod('div'); | ||||
|     errbox.setAttribute('id', 'md_errbox'); | ||||
|     errbox.style.cssText = 'position:absolute;top:0;left:0;padding:1em .5em;background:#2b2b2b;color:#fc5' | ||||
|     errbox.textContent = msg; | ||||
|   | ||||
| @@ -16,7 +16,7 @@ var dom_sbs = ebi('sbs'); | ||||
| var dom_nsbs = ebi('nsbs'); | ||||
| var dom_tbox = ebi('toolsbox'); | ||||
| var dom_ref = (function () { | ||||
|     var d = document.createElement('div'); | ||||
|     var d = mknod('div'); | ||||
|     d.setAttribute('id', 'mtr'); | ||||
|     dom_swrap.appendChild(d); | ||||
|     d = ebi('mtr'); | ||||
| @@ -71,7 +71,7 @@ var map_src = []; | ||||
| var map_pre = []; | ||||
| function genmap(dom, oldmap) { | ||||
|     var find = nlines; | ||||
|     while (oldmap && find --> 0) { | ||||
|     while (oldmap && find-- > 0) { | ||||
|         var tmap = genmapq(dom, '*[data-ln="' + find + '"]'); | ||||
|         if (!tmap || !tmap.length) | ||||
|             continue; | ||||
| @@ -94,7 +94,7 @@ var nlines = 0; | ||||
| var draw_md = (function () { | ||||
|     var delay = 1; | ||||
|     function draw_md() { | ||||
|         var t0 = new Date().getTime(); | ||||
|         var t0 = Date.now(); | ||||
|         var src = dom_src.value; | ||||
|         convert_markdown(src, dom_pre); | ||||
|  | ||||
| @@ -110,7 +110,7 @@ var draw_md = (function () { | ||||
|  | ||||
|         cls(ebi('save'), 'disabled', src == server_md); | ||||
|  | ||||
|         var t1 = new Date().getTime(); | ||||
|         var t1 = Date.now(); | ||||
|         delay = t1 - t0 > 100 ? 25 : 1; | ||||
|     } | ||||
|  | ||||
| @@ -252,7 +252,7 @@ function Modpoll() { | ||||
|         } | ||||
|  | ||||
|         console.log('modpoll...'); | ||||
|         var url = (document.location + '').split('?')[0] + '?raw&_=' + new Date().getTime(); | ||||
|         var url = (document.location + '').split('?')[0] + '?raw&_=' + Date.now(); | ||||
|         var xhr = new XMLHttpRequest(); | ||||
|         xhr.modpoll = this; | ||||
|         xhr.open('GET', url, true); | ||||
| @@ -399,7 +399,7 @@ function save_cb() { | ||||
|  | ||||
| function run_savechk(lastmod, txt, btn, ntry) { | ||||
|     // download the saved doc from the server and compare | ||||
|     var url = (document.location + '').split('?')[0] + '?raw&_=' + new Date().getTime(); | ||||
|     var url = (document.location + '').split('?')[0] + '?raw&_=' + Date.now(); | ||||
|     var xhr = new XMLHttpRequest(); | ||||
|     xhr.open('GET', url, true); | ||||
|     xhr.responseType = 'text'; | ||||
| @@ -455,7 +455,7 @@ function toast(autoclose, style, width, msg) { | ||||
|         ok.parentNode.removeChild(ok); | ||||
|  | ||||
|     style = "width:" + width + "em;left:calc(50% - " + (width / 2) + "em);" + style; | ||||
|     ok = document.createElement('div'); | ||||
|     ok = mknod('div'); | ||||
|     ok.setAttribute('id', 'toast'); | ||||
|     ok.setAttribute('style', style); | ||||
|     ok.innerHTML = msg; | ||||
| @@ -1049,7 +1049,7 @@ action_stack = (function () { | ||||
|         var p1 = from.length, | ||||
|             p2 = to.length; | ||||
|  | ||||
|         while (p1 --> 0 && p2 --> 0) | ||||
|         while (p1-- > 0 && p2-- > 0) | ||||
|             if (from[p1] != to[p2]) | ||||
|                 break; | ||||
|  | ||||
|   | ||||
| @@ -31,12 +31,12 @@ var md_opt = { | ||||
|  | ||||
| var lightswitch = (function () { | ||||
| 	var fun = function () { | ||||
| 		var dark = !!!document.documentElement.getAttribute("class"); | ||||
| 		var dark = !document.documentElement.getAttribute("class"); | ||||
| 		document.documentElement.setAttribute("class", dark ? "dark" : ""); | ||||
| 		if (window.localStorage) | ||||
| 			localStorage.setItem('darkmode', dark ? 1 : 0); | ||||
| 			localStorage.setItem('lightmode', dark ? 0 : 1); | ||||
| 	}; | ||||
| 	if (window.localStorage && localStorage.getItem('darkmode') == 1) | ||||
| 	if (window.localStorage && localStorage.getItem('lightmode') != 1) | ||||
| 		fun(); | ||||
| 	 | ||||
| 	return fun; | ||||
|   | ||||
| @@ -71,7 +71,7 @@ var mde = (function () { | ||||
| })(); | ||||
|  | ||||
| function set_jumpto() { | ||||
|     document.querySelector('.editor-preview-side').onclick = jumpto; | ||||
|     QS('.editor-preview-side').onclick = jumpto; | ||||
| } | ||||
|  | ||||
| function jumpto(ev) { | ||||
| @@ -94,7 +94,7 @@ function md_changed(mde, on_srv) { | ||||
|         window.md_saved = mde.value(); | ||||
|  | ||||
|     var md_now = mde.value(); | ||||
|     var save_btn = document.querySelector('.editor-toolbar button.save'); | ||||
|     var save_btn = QS('.editor-toolbar button.save'); | ||||
|  | ||||
|     if (md_now == window.md_saved) | ||||
|         save_btn.classList.add('disabled'); | ||||
| @@ -105,7 +105,7 @@ function md_changed(mde, on_srv) { | ||||
| } | ||||
|  | ||||
| function save(mde) { | ||||
|     var save_btn = document.querySelector('.editor-toolbar button.save'); | ||||
|     var save_btn = QS('.editor-toolbar button.save'); | ||||
|     if (save_btn.classList.contains('disabled')) { | ||||
|         alert('there is nothing to save'); | ||||
|         return; | ||||
| @@ -212,7 +212,7 @@ function save_chk() { | ||||
|     last_modified = this.lastmod; | ||||
|     md_changed(this.mde, true); | ||||
|  | ||||
|     var ok = document.createElement('div'); | ||||
|     var ok = mknod('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.innerHTML = 'OK✔️'; | ||||
|     var parent = ebi('m'); | ||||
|   | ||||
| @@ -38,7 +38,7 @@ | ||||
|     </div> | ||||
|     <script> | ||||
|  | ||||
| if (window.localStorage && localStorage.getItem('darkmode') == 1) | ||||
| if (window.localStorage && localStorage.getItem('lightmode') != 1) | ||||
|     document.documentElement.setAttribute("class", "dark"); | ||||
|  | ||||
| </script> | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -47,6 +47,11 @@ | ||||
| 	margin: -1.5em 0; | ||||
| 	padding: .8em 0; | ||||
| 	width: 100%; | ||||
| 	max-width: 12em; | ||||
| 	display: inline-block; | ||||
| } | ||||
| #u2conf #u2btn_cw { | ||||
| 	text-align: right; | ||||
| } | ||||
| #u2notbtn { | ||||
| 	display: none; | ||||
| @@ -72,6 +77,7 @@ | ||||
| } | ||||
| #u2tab td:nth-child(2) { | ||||
| 	width: 5em; | ||||
| 	white-space: nowrap; | ||||
| } | ||||
| #u2tab td:nth-child(3) { | ||||
| 	width: 40%; | ||||
| @@ -83,6 +89,42 @@ | ||||
| #u2tab tr+tr:hover td { | ||||
| 	background: #222; | ||||
| } | ||||
| #u2cards { | ||||
| 	padding: 1em 0 .3em 1em; | ||||
| 	margin: 1.5em auto -2.5em auto; | ||||
| 	text-align: center; | ||||
| 	overflow: hidden; | ||||
| } | ||||
| #u2cards.w { | ||||
| 	width: 45em; | ||||
| 	text-align: left; | ||||
| } | ||||
| #u2cards a { | ||||
| 	padding: .2em 1em; | ||||
| 	border: 1px solid #777; | ||||
| 	border-width: 0 0 1px 0; | ||||
| 	background: linear-gradient(to bottom, #333, #222); | ||||
| } | ||||
| #u2cards a:first-child { | ||||
| 	border-radius: .4em 0 0 0; | ||||
| } | ||||
| #u2cards a:last-child { | ||||
| 	border-radius: 0 .4em 0 0; | ||||
| } | ||||
| #u2cards a.act { | ||||
| 	padding-bottom: .5em; | ||||
| 	border-width: 1px 1px .1em 1px; | ||||
| 	border-radius: .3em .3em 0 0; | ||||
| 	margin-left: -1px; | ||||
| 	background: linear-gradient(to bottom, #464, #333 80%); | ||||
| 	box-shadow: 0 -.17em .67em #280; | ||||
| 	border-color: #7c5 #583 #333 #583; | ||||
| 	position: relative; | ||||
| 	color: #fd7; | ||||
| } | ||||
| #u2cards span { | ||||
| 	color: #fff; | ||||
| } | ||||
| #u2conf { | ||||
| 	margin: 1em auto; | ||||
| 	width: 30em; | ||||
| @@ -99,12 +141,16 @@ | ||||
| 	outline: none; | ||||
| } | ||||
| #u2conf .txtbox { | ||||
| 	width: 4em; | ||||
| 	width: 3em; | ||||
| 	color: #fff; | ||||
| 	background: #444; | ||||
| 	border: 1px solid #777; | ||||
| 	font-size: 1.2em; | ||||
| 	padding: .15em 0; | ||||
| 	height: 1.05em; | ||||
| } | ||||
| #u2conf .txtbox.err { | ||||
| 	background: #922; | ||||
| } | ||||
| #u2conf a { | ||||
| 	color: #fff; | ||||
| @@ -113,13 +159,12 @@ | ||||
| 	border-radius: .1em; | ||||
| 	font-size: 1.5em; | ||||
| 	padding: .1em 0; | ||||
| 	margin: 0 -.25em; | ||||
| 	margin: 0 -1px; | ||||
| 	width: 1.5em; | ||||
| 	height: 1em; | ||||
| 	display: inline-block; | ||||
| 	position: relative; | ||||
| 	line-height: 1em; | ||||
| 	bottom: -.08em; | ||||
| 	bottom: -0.08em; | ||||
| } | ||||
| #u2conf input+a { | ||||
| 	background: #d80; | ||||
| @@ -130,7 +175,6 @@ | ||||
| 	height: 1em; | ||||
| 	padding: .4em 0; | ||||
| 	display: block; | ||||
| 	user-select: none; | ||||
| 	border-radius: .25em; | ||||
| } | ||||
| #u2conf input[type="checkbox"] { | ||||
| @@ -170,12 +214,13 @@ | ||||
| 	text-align: center; | ||||
| 	overflow: hidden; | ||||
| 	margin: 0 -2em; | ||||
| 	height: 0; | ||||
| 	padding: 0 1em; | ||||
| 	height: 0; | ||||
| 	opacity: .1; | ||||
|     transition: all 0.14s ease-in-out; | ||||
| 	border-radius: .4em; | ||||
| 	box-shadow: 0 .2em .5em #222; | ||||
| 	border-radius: .4em; | ||||
| 	z-index: 1; | ||||
| } | ||||
| #u2cdesc.show { | ||||
| 	padding: 1em; | ||||
| @@ -193,24 +238,6 @@ | ||||
| .prog { | ||||
| 	font-family: monospace; | ||||
| } | ||||
| .prog>div { | ||||
| 	display: inline-block; | ||||
| 	position: relative; | ||||
| 	overflow: hidden; | ||||
| 	margin: 0; | ||||
| 	padding: 0; | ||||
| 	height: 1.1em; | ||||
| 	margin-bottom: -.15em; | ||||
| 	box-shadow: -1px -1px 0 inset rgba(255,255,255,0.1); | ||||
| } | ||||
| .prog>div>div { | ||||
| 	width: 0%; | ||||
| 	position: absolute; | ||||
| 	left: 0; | ||||
| 	top: 0; | ||||
| 	bottom: 0; | ||||
| 	background: #0a0; | ||||
| } | ||||
| #u2tab a>span { | ||||
| 	font-weight: bold; | ||||
| 	font-style: italic; | ||||
| @@ -221,3 +248,41 @@ | ||||
| 	float: right; | ||||
| 	margin-bottom: -.3em; | ||||
| } | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| html.light #u2btn { | ||||
| 	box-shadow: .4em .4em 0 #ccc; | ||||
| } | ||||
| html.light #u2cards span { | ||||
| 	color: #000; | ||||
| } | ||||
| html.light #u2cards a { | ||||
| 	background: linear-gradient(to bottom, #eee, #fff); | ||||
| } | ||||
| html.light #u2cards a.act { | ||||
| 	color: #037; | ||||
| 	background: inherit; | ||||
| 	box-shadow: 0 -.17em .67em #0ad; | ||||
| 	border-color: #09c #05a #eee #05a; | ||||
| } | ||||
| html.light #u2conf .txtbox { | ||||
| 	background: #fff; | ||||
| 	color: #444; | ||||
| } | ||||
| html.light #u2conf .txtbox.err { | ||||
| 	background: #f96; | ||||
| 	color: #300; | ||||
| } | ||||
| html.light #u2cdesc { | ||||
| 	background: #fff; | ||||
| 	border: none; | ||||
| } | ||||
| html.light #op_up2k.srch #u2btn { | ||||
| 	border-color: #a80; | ||||
| } | ||||
| html.light #u2foot { | ||||
| 	color: #000; | ||||
| } | ||||
|   | ||||
| @@ -59,9 +59,9 @@ | ||||
|                 </tr> | ||||
|                 <tr> | ||||
|                     <td> | ||||
|                         <a href="#" id="nthread_sub">–</a> | ||||
|                         <input class="txtbox" id="nthread" value="2" /> | ||||
|                         <a href="#" id="nthread_add">+</a> | ||||
|                         <a href="#" id="nthread_sub">–</a><input | ||||
|                             class="txtbox" id="nthread" value="2"/><a | ||||
|                             href="#" id="nthread_add">+</a> | ||||
|                     </td> | ||||
|                 </tr> | ||||
|             </table> | ||||
| @@ -79,12 +79,23 @@ | ||||
|                 </div> | ||||
|             </div> | ||||
|  | ||||
|             <div id="u2cards"> | ||||
|                 <a href="#" act="ok">ok <span>0</span></a><a | ||||
|                 href="#" act="ng">ng <span>0</span></a><a | ||||
|                 href="#" act="done">done <span>0</span></a><a | ||||
|                 href="#" act="bz" class="act">busy <span>0</span></a><a | ||||
|                 href="#" act="q">que <span>0</span></a> | ||||
|             </div> | ||||
|  | ||||
|             <table id="u2tab"> | ||||
|                 <tr> | ||||
|                     <td>filename</td> | ||||
|                     <td>status</td> | ||||
|                     <td>progress<a href="#" id="u2cleanup">cleanup</a></td> | ||||
|                 </tr> | ||||
|                 <thead> | ||||
|                     <tr> | ||||
|                         <td>filename</td> | ||||
|                         <td>status</td> | ||||
|                         <td>progress<a href="#" id="u2cleanup">cleanup</a></td> | ||||
|                     </tr> | ||||
|                 </thead> | ||||
|                 <tbody></tbody> | ||||
|             </table> | ||||
|  | ||||
|             <p id="u2foot"></p> | ||||
|   | ||||
| @@ -6,7 +6,8 @@ if (!window['console']) | ||||
|     }; | ||||
|  | ||||
|  | ||||
| var clickev = window.Touch ? 'touchstart' : 'click'; | ||||
| var clickev = window.Touch ? 'touchstart' : 'click', | ||||
|     ANDROID = /(android)/i.test(navigator.userAgent); | ||||
|  | ||||
|  | ||||
| // error handler for mobile devices | ||||
| @@ -49,9 +50,11 @@ function vis_exh(msg, url, lineNo, columnNo, error) { | ||||
| } | ||||
|  | ||||
|  | ||||
| function ebi(id) { | ||||
|     return document.getElementById(id); | ||||
| } | ||||
| var ebi = document.getElementById.bind(document), | ||||
|     QS = document.querySelector.bind(document), | ||||
|     QSA = document.querySelectorAll.bind(document), | ||||
|     mknod = document.createElement.bind(document); | ||||
|  | ||||
|  | ||||
| function ev(e) { | ||||
|     e = e || window.event; | ||||
| @@ -89,7 +92,7 @@ if (!String.startsWith) { | ||||
| // https://stackoverflow.com/a/950146 | ||||
| function import_js(url, cb) { | ||||
|     var head = document.head || document.getElementsByTagName('head')[0]; | ||||
|     var script = document.createElement('script'); | ||||
|     var script = mknod('script'); | ||||
|     script.type = 'text/javascript'; | ||||
|     script.src = url; | ||||
|  | ||||
| @@ -274,7 +277,7 @@ function makeSortable(table, cb) { | ||||
|  | ||||
|  | ||||
| (function () { | ||||
|     var ops = document.querySelectorAll('#ops>a'); | ||||
|     var ops = QSA('#ops>a'); | ||||
|     for (var a = 0; a < ops.length; a++) { | ||||
|         ops[a].onclick = opclick; | ||||
|     } | ||||
| @@ -289,25 +292,25 @@ function opclick(e) { | ||||
|  | ||||
|     swrite('opmode', dest || null); | ||||
|  | ||||
|     var input = document.querySelector('.opview.act input:not([type="hidden"])') | ||||
|     var input = QS('.opview.act input:not([type="hidden"])') | ||||
|     if (input) | ||||
|         input.focus(); | ||||
| } | ||||
|  | ||||
|  | ||||
| function goto(dest) { | ||||
|     var obj = document.querySelectorAll('.opview.act'); | ||||
|     var obj = QSA('.opview.act'); | ||||
|     for (var a = obj.length - 1; a >= 0; a--) | ||||
|         clmod(obj[a], 'act'); | ||||
|  | ||||
|     obj = document.querySelectorAll('#ops>a'); | ||||
|     obj = QSA('#ops>a'); | ||||
|     for (var a = obj.length - 1; a >= 0; a--) | ||||
|         clmod(obj[a], 'act'); | ||||
|  | ||||
|     if (dest) { | ||||
|         var ui = ebi('op_' + dest); | ||||
|         clmod(ui, 'act', true); | ||||
|         document.querySelector('#ops>a[data-dest=' + dest + ']').className += " act"; | ||||
|         QS('#ops>a[data-dest=' + dest + ']').className += " act"; | ||||
|  | ||||
|         var fn = window['goto_' + dest]; | ||||
|         if (fn) | ||||
|   | ||||
| @@ -171,7 +171,7 @@ Range: bytes=26-         Content-Range: bytes */26 | ||||
|  | ||||
| var tsh = []; | ||||
| function convert_markdown(md_text, dest_dom) { | ||||
|     tsh.push(new Date().getTime()); | ||||
|     tsh.push(Date.now()); | ||||
|     while (tsh.length > 10) | ||||
|         tsh.shift(); | ||||
|     if (tsh.length > 1) { | ||||
|   | ||||
| @@ -26,7 +26,7 @@ CKSUM = None | ||||
| STAMP = None | ||||
|  | ||||
| PY2 = sys.version_info[0] == 2 | ||||
| WINDOWS = sys.platform == "win32" | ||||
| WINDOWS = sys.platform in ["win32", "msys"] | ||||
| sys.dont_write_bytecode = True | ||||
| me = os.path.abspath(os.path.realpath(__file__)) | ||||
| cpp = None | ||||
|   | ||||
							
								
								
									
										0
									
								
								tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										33
									
								
								tests/run.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										33
									
								
								tests/run.py
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,33 @@ | ||||
| #!/usr/bin/env python3 | ||||
|  | ||||
| import sys | ||||
| import runpy | ||||
|  | ||||
| host = sys.argv[1] | ||||
| sys.argv = sys.argv[:1] + sys.argv[2:] | ||||
| sys.path.insert(0, ".") | ||||
|  | ||||
|  | ||||
| def rp(): | ||||
|     runpy.run_module("unittest", run_name="__main__") | ||||
|  | ||||
|  | ||||
| if host == "vmprof": | ||||
|     rp() | ||||
|  | ||||
| elif host == "cprofile": | ||||
|     import cProfile | ||||
|     import pstats | ||||
|  | ||||
|     log_fn = "cprofile.log" | ||||
|     cProfile.run("rp()", log_fn) | ||||
|     p = pstats.Stats(log_fn) | ||||
|     p.sort_stats(pstats.SortKey.CUMULATIVE).print_stats(64) | ||||
|  | ||||
|  | ||||
| """ | ||||
| python3.9 tests/run.py cprofile -v tests/test_httpcli.py | ||||
|  | ||||
| python3.9 -m pip install --user vmprof | ||||
| python3.9 -m vmprof --lines -o vmprof.log tests/run.py vmprof -v tests/test_httpcli.py | ||||
| """ | ||||
							
								
								
									
										202
									
								
								tests/test_httpcli.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										202
									
								
								tests/test_httpcli.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,202 @@ | ||||
| #!/usr/bin/env python | ||||
| # coding: utf-8 | ||||
| from __future__ import print_function, unicode_literals | ||||
|  | ||||
| import io | ||||
| import os | ||||
| import time | ||||
| import shutil | ||||
| import pprint | ||||
| import tarfile | ||||
| import unittest | ||||
|  | ||||
| from argparse import Namespace | ||||
| from copyparty.authsrv import AuthSrv | ||||
| from copyparty.httpcli import HttpCli | ||||
|  | ||||
| from tests import util as tu | ||||
|  | ||||
|  | ||||
| def hdr(query): | ||||
|     h = "GET /{} HTTP/1.1\r\nCookie: cppwd=o\r\nConnection: close\r\n\r\n" | ||||
|     return h.format(query).encode("utf-8") | ||||
|  | ||||
|  | ||||
| class Cfg(Namespace): | ||||
|     def __init__(self, a=[], v=[], c=None): | ||||
|         super(Cfg, self).__init__( | ||||
|             a=a, | ||||
|             v=v, | ||||
|             c=c, | ||||
|             ed=False, | ||||
|             no_zip=False, | ||||
|             no_scandir=False, | ||||
|             no_sendfile=True, | ||||
|             nih=True, | ||||
|             mtp=[], | ||||
|             mte="a", | ||||
|             **{k: False for k in "e2d e2ds e2dsa e2t e2ts e2tsr".split()} | ||||
|         ) | ||||
|  | ||||
|  | ||||
| class TestHttpCli(unittest.TestCase): | ||||
|     def test(self): | ||||
|         td = os.path.join(tu.get_ramdisk(), "vfs") | ||||
|         try: | ||||
|             shutil.rmtree(td) | ||||
|         except OSError: | ||||
|             pass | ||||
|  | ||||
|         os.mkdir(td) | ||||
|         os.chdir(td) | ||||
|  | ||||
|         self.dtypes = ["ra", "ro", "rx", "wa", "wo", "wx", "aa", "ao", "ax"] | ||||
|         self.can_read = ["ra", "ro", "aa", "ao"] | ||||
|         self.can_write = ["wa", "wo", "aa", "ao"] | ||||
|         self.fn = "g{:x}g".format(int(time.time() * 3)) | ||||
|  | ||||
|         allfiles = [] | ||||
|         allvols = [] | ||||
|         for top in self.dtypes: | ||||
|             allvols.append(top) | ||||
|             allfiles.append("/".join([top, self.fn])) | ||||
|             for s1 in self.dtypes: | ||||
|                 p = "/".join([top, s1]) | ||||
|                 allvols.append(p) | ||||
|                 allfiles.append(p + "/" + self.fn) | ||||
|                 allfiles.append(p + "/n/" + self.fn) | ||||
|                 for s2 in self.dtypes: | ||||
|                     p = "/".join([top, s1, "n", s2]) | ||||
|                     os.makedirs(p) | ||||
|                     allvols.append(p) | ||||
|                     allfiles.append(p + "/" + self.fn) | ||||
|  | ||||
|         for fp in allfiles: | ||||
|             with open(fp, "w") as f: | ||||
|                 f.write("ok {}\n".format(fp)) | ||||
|  | ||||
|         for top in self.dtypes: | ||||
|             vcfg = [] | ||||
|             for vol in allvols: | ||||
|                 if not vol.startswith(top): | ||||
|                     continue | ||||
|  | ||||
|                 mode = vol[-2] | ||||
|                 usr = vol[-1] | ||||
|                 if usr == "a": | ||||
|                     usr = "" | ||||
|  | ||||
|                 if "/" not in vol: | ||||
|                     vol += "/" | ||||
|  | ||||
|                 top, sub = vol.split("/", 1) | ||||
|                 vcfg.append("{0}/{1}:{1}:{2}{3}".format(top, sub, mode, usr)) | ||||
|  | ||||
|             pprint.pprint(vcfg) | ||||
|  | ||||
|             self.args = Cfg(v=vcfg, a=["o:o", "x:x"]) | ||||
|             self.auth = AuthSrv(self.args, self.log) | ||||
|             vfiles = [x for x in allfiles if x.startswith(top)] | ||||
|             for fp in vfiles: | ||||
|                 rok, wok = self.can_rw(fp) | ||||
|                 furl = fp.split("/", 1)[1] | ||||
|                 durl = furl.rsplit("/", 1)[0] if "/" in furl else "" | ||||
|  | ||||
|                 # file download | ||||
|                 h, ret = self.curl(furl) | ||||
|                 res = "ok " + fp in ret | ||||
|                 print("[{}] {} {} = {}".format(fp, rok, wok, res)) | ||||
|                 if rok != res: | ||||
|                     print("\033[33m{}\n# {}\033[0m".format(ret, furl)) | ||||
|                     self.fail() | ||||
|  | ||||
|                 # file browser: html | ||||
|                 h, ret = self.curl(durl) | ||||
|                 res = "'{}'".format(self.fn) in ret | ||||
|                 print(res) | ||||
|                 if rok != res: | ||||
|                     print("\033[33m{}\n# {}\033[0m".format(ret, durl)) | ||||
|                     self.fail() | ||||
|  | ||||
|                 # file browser: json | ||||
|                 url = durl + "?ls" | ||||
|                 h, ret = self.curl(url) | ||||
|                 res = '"{}"'.format(self.fn) in ret | ||||
|                 print(res) | ||||
|                 if rok != res: | ||||
|                     print("\033[33m{}\n# {}\033[0m".format(ret, url)) | ||||
|                     self.fail() | ||||
|  | ||||
|                 # tar | ||||
|                 url = durl + "?tar" | ||||
|                 h, b = self.curl(url, True) | ||||
|                 # with open(os.path.join(td, "tar"), "wb") as f: | ||||
|                 #    f.write(b) | ||||
|                 try: | ||||
|                     tar = tarfile.open(fileobj=io.BytesIO(b)).getnames() | ||||
|                 except: | ||||
|                     tar = [] | ||||
|                 tar = ["/".join([y for y in [top, durl, x] if y]) for x in tar] | ||||
|                 tar = [[x] + self.can_rw(x) for x in tar] | ||||
|                 tar_ok = [x[0] for x in tar if x[1]] | ||||
|                 tar_ng = [x[0] for x in tar if not x[1]] | ||||
|                 self.assertEqual([], tar_ng) | ||||
|  | ||||
|                 if durl.split("/")[-1] in self.can_read: | ||||
|                     ref = [x for x in vfiles if self.in_dive(top + "/" + durl, x)] | ||||
|                     for f in ref: | ||||
|                         print("{}: {}".format("ok" if f in tar_ok else "NG", f)) | ||||
|                     ref.sort() | ||||
|                     tar_ok.sort() | ||||
|                     self.assertEqual(ref, tar_ok) | ||||
|  | ||||
|                 # stash | ||||
|                 h, ret = self.put(url) | ||||
|                 res = h.startswith("HTTP/1.1 200 ") | ||||
|                 self.assertEqual(res, wok) | ||||
|  | ||||
|     def can_rw(self, fp): | ||||
|         # lowest non-neutral folder declares permissions | ||||
|         expect = fp.split("/")[:-1] | ||||
|         for x in reversed(expect): | ||||
|             if x != "n": | ||||
|                 expect = x | ||||
|                 break | ||||
|  | ||||
|         return [expect in self.can_read, expect in self.can_write] | ||||
|  | ||||
|     def in_dive(self, top, fp): | ||||
|         # archiver bails at first inaccessible subvolume | ||||
|         top = top.strip("/").split("/") | ||||
|         fp = fp.split("/") | ||||
|         for f1, f2 in zip(top, fp): | ||||
|             if f1 != f2: | ||||
|                 return False | ||||
|  | ||||
|         for f in fp[len(top) :]: | ||||
|             if f == self.fn: | ||||
|                 return True | ||||
|             if f not in self.can_read and f != "n": | ||||
|                 return False | ||||
|  | ||||
|         return True | ||||
|  | ||||
|     def put(self, url): | ||||
|         buf = "PUT /{0} HTTP/1.1\r\nCookie: cppwd=o\r\nConnection: close\r\nContent-Length: {1}\r\n\r\nok {0}\n" | ||||
|         buf = buf.format(url, len(url) + 4).encode("utf-8") | ||||
|         conn = tu.VHttpConn(self.args, self.auth, self.log, buf) | ||||
|         HttpCli(conn).run() | ||||
|         return conn.s._reply.decode("utf-8").split("\r\n\r\n", 1) | ||||
|  | ||||
|     def curl(self, url, binary=False): | ||||
|         conn = tu.VHttpConn(self.args, self.auth, self.log, hdr(url)) | ||||
|         HttpCli(conn).run() | ||||
|         if binary: | ||||
|             h, b = conn.s._reply.split(b"\r\n\r\n", 1) | ||||
|             return [h.decode("utf-8"), b] | ||||
|  | ||||
|         return conn.s._reply.decode("utf-8").split("\r\n\r\n", 1) | ||||
|  | ||||
|     def log(self, src, msg, c=0): | ||||
|         # print(repr(msg)) | ||||
|         pass | ||||
| @@ -3,18 +3,18 @@ | ||||
| from __future__ import print_function, unicode_literals | ||||
|  | ||||
| import os | ||||
| import time | ||||
| import json | ||||
| import shutil | ||||
| import tempfile | ||||
| import unittest | ||||
| import subprocess as sp  # nosec | ||||
|  | ||||
| from textwrap import dedent | ||||
| from argparse import Namespace | ||||
| from copyparty.authsrv import AuthSrv | ||||
| from copyparty import util | ||||
|  | ||||
| from tests import util as tu | ||||
|  | ||||
|  | ||||
| class Cfg(Namespace): | ||||
|     def __init__(self, a=[], v=[], c=None): | ||||
| @@ -51,52 +51,11 @@ class TestVFS(unittest.TestCase): | ||||
|         real = [x[0] for x in real] | ||||
|         return fsdir, real, virt | ||||
|  | ||||
|     def runcmd(self, *argv): | ||||
|         p = sp.Popen(argv, stdout=sp.PIPE, stderr=sp.PIPE) | ||||
|         stdout, stderr = p.communicate() | ||||
|         stdout = stdout.decode("utf-8") | ||||
|         stderr = stderr.decode("utf-8") | ||||
|         return [p.returncode, stdout, stderr] | ||||
|  | ||||
|     def chkcmd(self, *argv): | ||||
|         ok, sout, serr = self.runcmd(*argv) | ||||
|         if ok != 0: | ||||
|             raise Exception(serr) | ||||
|  | ||||
|         return sout, serr | ||||
|  | ||||
|     def get_ramdisk(self): | ||||
|         for vol in ["/dev/shm", "/Volumes/cptd"]:  # nosec (singleton test) | ||||
|             if os.path.exists(vol): | ||||
|                 return vol | ||||
|  | ||||
|         if os.path.exists("/Volumes"): | ||||
|             devname, _ = self.chkcmd("hdiutil", "attach", "-nomount", "ram://8192") | ||||
|             devname = devname.strip() | ||||
|             print("devname: [{}]".format(devname)) | ||||
|             for _ in range(10): | ||||
|                 try: | ||||
|                     _, _ = self.chkcmd( | ||||
|                         "diskutil", "eraseVolume", "HFS+", "cptd", devname | ||||
|                     ) | ||||
|                     return "/Volumes/cptd" | ||||
|                 except Exception as ex: | ||||
|                     print(repr(ex)) | ||||
|                     time.sleep(0.25) | ||||
|  | ||||
|             raise Exception("ramdisk creation failed") | ||||
|  | ||||
|         ret = os.path.join(tempfile.gettempdir(), "copyparty-test") | ||||
|         try: | ||||
|             os.mkdir(ret) | ||||
|         finally: | ||||
|             return ret | ||||
|  | ||||
|     def log(self, src, msg, c=0): | ||||
|         pass | ||||
|  | ||||
|     def test(self): | ||||
|         td = os.path.join(self.get_ramdisk(), "vfs") | ||||
|         td = os.path.join(tu.get_ramdisk(), "vfs") | ||||
|         try: | ||||
|             shutil.rmtree(td) | ||||
|         except OSError: | ||||
| @@ -268,7 +227,7 @@ class TestVFS(unittest.TestCase): | ||||
|         self.assertEqual(list(v1), list(v2)) | ||||
|  | ||||
|         # config file parser | ||||
|         cfg_path = os.path.join(self.get_ramdisk(), "test.cfg") | ||||
|         cfg_path = os.path.join(tu.get_ramdisk(), "test.cfg") | ||||
|         with open(cfg_path, "wb") as f: | ||||
|             f.write( | ||||
|                 dedent( | ||||
|   | ||||
							
								
								
									
										97
									
								
								tests/util.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								tests/util.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,97 @@ | ||||
| import os | ||||
| import time | ||||
| import jinja2 | ||||
| import tempfile | ||||
| import subprocess as sp | ||||
|  | ||||
| from copyparty.util import Unrecv | ||||
|  | ||||
|  | ||||
| J2_ENV = jinja2.Environment(loader=jinja2.BaseLoader) | ||||
| J2_FILES = J2_ENV.from_string("{{ files|join('\n') }}") | ||||
|  | ||||
|  | ||||
| def runcmd(*argv): | ||||
|     p = sp.Popen(argv, stdout=sp.PIPE, stderr=sp.PIPE) | ||||
|     stdout, stderr = p.communicate() | ||||
|     stdout = stdout.decode("utf-8") | ||||
|     stderr = stderr.decode("utf-8") | ||||
|     return [p.returncode, stdout, stderr] | ||||
|  | ||||
|  | ||||
| def chkcmd(*argv): | ||||
|     ok, sout, serr = runcmd(*argv) | ||||
|     if ok != 0: | ||||
|         raise Exception(serr) | ||||
|  | ||||
|     return sout, serr | ||||
|  | ||||
|  | ||||
| def get_ramdisk(): | ||||
|     for vol in ["/dev/shm", "/Volumes/cptd"]:  # nosec (singleton test) | ||||
|         if os.path.exists(vol): | ||||
|             return vol | ||||
|  | ||||
|     if os.path.exists("/Volumes"): | ||||
|         devname, _ = chkcmd("hdiutil", "attach", "-nomount", "ram://32768") | ||||
|         devname = devname.strip() | ||||
|         print("devname: [{}]".format(devname)) | ||||
|         for _ in range(10): | ||||
|             try: | ||||
|                 _, _ = chkcmd("diskutil", "eraseVolume", "HFS+", "cptd", devname) | ||||
|                 return "/Volumes/cptd" | ||||
|             except Exception as ex: | ||||
|                 print(repr(ex)) | ||||
|                 time.sleep(0.25) | ||||
|  | ||||
|         raise Exception("ramdisk creation failed") | ||||
|  | ||||
|     ret = os.path.join(tempfile.gettempdir(), "copyparty-test") | ||||
|     try: | ||||
|         os.mkdir(ret) | ||||
|     finally: | ||||
|         return ret | ||||
|  | ||||
|  | ||||
| class NullBroker(object): | ||||
|     def put(*args): | ||||
|         pass | ||||
|  | ||||
|  | ||||
| class VSock(object): | ||||
|     def __init__(self, buf): | ||||
|         self._query = buf | ||||
|         self._reply = b"" | ||||
|         self.sendall = self.send | ||||
|  | ||||
|     def recv(self, sz): | ||||
|         ret = self._query[:sz] | ||||
|         self._query = self._query[sz:] | ||||
|         return ret | ||||
|  | ||||
|     def send(self, buf): | ||||
|         self._reply += buf | ||||
|         return len(buf) | ||||
|  | ||||
|  | ||||
| class VHttpSrv(object): | ||||
|     def __init__(self): | ||||
|         self.broker = NullBroker() | ||||
|  | ||||
|         aliases = ["splash", "browser", "browser2", "msg", "md", "mde"] | ||||
|         self.j2 = {x: J2_FILES for x in aliases} | ||||
|  | ||||
|  | ||||
| class VHttpConn(object): | ||||
|     def __init__(self, args, auth, log, buf): | ||||
|         self.s = VSock(buf) | ||||
|         self.sr = Unrecv(self.s) | ||||
|         self.addr = ("127.0.0.1", "42069") | ||||
|         self.args = args | ||||
|         self.auth = auth | ||||
|         self.log_func = log | ||||
|         self.log_src = "a" | ||||
|         self.hsrv = VHttpSrv() | ||||
|         self.nbyte = 0 | ||||
|         self.workload = 0 | ||||
|         self.t0 = time.time() | ||||
		Reference in New Issue
	
	Block a user