mirror of
				https://github.com/9001/copyparty.git
				synced 2025-10-31 03:53:31 +00:00 
			
		
		
		
	Compare commits
	
		
			89 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 0a3bbc4b4a | ||
|  | 855b93dcf6 | ||
|  | 89b79ba267 | ||
|  | f5651b7d94 | ||
|  | 1881019ede | ||
|  | caba4e974c | ||
|  | bc3c9613bc | ||
|  | 15a3ee252e | ||
|  | be055961ae | ||
|  | e3031bdeec | ||
|  | 75917b9f7c | ||
|  | 910732e02c | ||
|  | 264b497681 | ||
|  | 372b949622 | ||
|  | 789a602914 | ||
|  | 093e955100 | ||
|  | c32a89bebf | ||
|  | c0bebe9f9f | ||
|  | 57579b2fe5 | ||
|  | 51d14a6b4d | ||
|  | c50f1b64e5 | ||
|  | 98aaab02c5 | ||
|  | 0fc7973d8b | ||
|  | 10362aa02e | ||
|  | 0a8e759fe6 | ||
|  | d70981cdd1 | ||
|  | e08c03b886 | ||
|  | 56086e8984 | ||
|  | 1aa9033022 | ||
|  | 076e103d53 | ||
|  | 38c00ea8fc | ||
|  | 415757af43 | ||
|  | e72ed8c0ed | ||
|  | 32f9c6b5bb | ||
|  | 6251584ef6 | ||
|  | f3e413bc28 | ||
|  | 6f6cc8f3f8 | ||
|  | 8b081e9e69 | ||
|  | c8a510d10e | ||
|  | 6f834f6679 | ||
|  | cf2d6650ac | ||
|  | cd52dea488 | ||
|  | 6ea75df05d | ||
|  | 4846e1e8d6 | ||
|  | fc024f789d | ||
|  | 473e773aea | ||
|  | 48a2e1a353 | ||
|  | 6da63fbd79 | ||
|  | 5bec37fcee | ||
|  | 3fd0ba0a31 | ||
|  | 241a143366 | ||
|  | a537064da7 | ||
|  | f3dfd24c92 | ||
|  | fa0a7f50bb | ||
|  | 44a78a7e21 | ||
|  | 6b75cbf747 | ||
|  | e7b18ab9fe | ||
|  | aa12830015 | ||
|  | f156e00064 | ||
|  | d53c212516 | ||
|  | ca27f8587c | ||
|  | 88ce008e16 | ||
|  | 081d2cc5d7 | ||
|  | 60ac68d000 | ||
|  | fbe656957d | ||
|  | 5534c78c17 | ||
|  | a45a53fdce | ||
|  | 972a56e738 | ||
|  | 5e03b3ca38 | ||
|  | 1078d933b4 | ||
|  | d6bf300d80 | ||
|  | a359d64d44 | ||
|  | 22396e8c33 | ||
|  | 5ded5a4516 | ||
|  | 79c7639aaf | ||
|  | 5bbf875385 | ||
|  | 5e159432af | ||
|  | 1d6ae409f6 | ||
|  | 9d729d3d1a | ||
|  | 4dd5d4e1b7 | ||
|  | acd8149479 | ||
|  | b97a1088fa | ||
|  | b77bed3324 | ||
|  | a2b7c85a1f | ||
|  | b28533f850 | ||
|  | bd8c7e538a | ||
|  | 89e48cff24 | ||
|  | ae90a7b7b6 | ||
|  | 6fc1be04da | 
							
								
								
									
										17
									
								
								.vscode/launch.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										17
									
								
								.vscode/launch.json
									
									
									
									
										vendored
									
									
								
							| @@ -16,12 +16,9 @@ | ||||
|                 "-e2ts", | ||||
|                 "-mtp", | ||||
|                 ".bpm=f,bin/mtag/audio-bpm.py", | ||||
|                 "-a", | ||||
|                 "ed:wark", | ||||
|                 "-v", | ||||
|                 "srv::r:aed:cnodupe", | ||||
|                 "-v", | ||||
|                 "dist:dist:r" | ||||
|                 "-aed:wark", | ||||
|                 "-vsrv::r:aed:cnodupe", | ||||
|                 "-vdist:dist:r" | ||||
|             ] | ||||
|         }, | ||||
|         { | ||||
| @@ -43,5 +40,13 @@ | ||||
|                 "${file}" | ||||
|             ] | ||||
|         }, | ||||
|         { | ||||
|             "name": "Python: Current File", | ||||
|             "type": "python", | ||||
|             "request": "launch", | ||||
|             "program": "${file}", | ||||
|             "console": "integratedTerminal", | ||||
|             "justMyCode": false | ||||
|         }, | ||||
|     ] | ||||
| } | ||||
							
								
								
									
										28
									
								
								.vscode/launch.py
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										28
									
								
								.vscode/launch.py
									
									
									
									
										vendored
									
									
								
							| @@ -3,14 +3,16 @@ | ||||
| # launches 10x faster than mspython debugpy | ||||
| # and is stoppable with ^C | ||||
|  | ||||
| import re | ||||
| import os | ||||
| import sys | ||||
|  | ||||
| print(sys.executable) | ||||
|  | ||||
| import shlex | ||||
|  | ||||
| sys.path.insert(0, os.getcwd()) | ||||
|  | ||||
| import jstyleson | ||||
| from copyparty.__main__ import main as copyparty | ||||
| import subprocess as sp | ||||
|  | ||||
|  | ||||
| with open(".vscode/launch.json", "r", encoding="utf-8") as f: | ||||
|     tj = f.read() | ||||
| @@ -25,11 +27,19 @@ except: | ||||
|     pass | ||||
|  | ||||
| argv = [os.path.expanduser(x) if x.startswith("~") else x for x in argv] | ||||
| try: | ||||
|     copyparty(["a"] + argv) | ||||
| except SystemExit as ex: | ||||
|     if ex.code: | ||||
|         raise | ||||
|  | ||||
| if re.search(" -j ?[0-9]", " ".join(argv)): | ||||
|     argv = [sys.executable, "-m", "copyparty"] + argv | ||||
|     sp.check_call(argv) | ||||
| else: | ||||
|     sys.path.insert(0, os.getcwd()) | ||||
|     from copyparty.__main__ import main as copyparty | ||||
|  | ||||
|     try: | ||||
|         copyparty(["a"] + argv) | ||||
|     except SystemExit as ex: | ||||
|         if ex.code: | ||||
|             raise | ||||
|  | ||||
| print("\n\033[32mokke\033[0m") | ||||
| sys.exit(1) | ||||
|   | ||||
							
								
								
									
										96
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										96
									
								
								README.md
									
									
									
									
									
								
							| @@ -37,6 +37,7 @@ turn your phone or raspi into a portable file server with resumable uploads/down | ||||
|     * [other tricks](#other-tricks) | ||||
| * [searching](#searching) | ||||
|     * [search configuration](#search-configuration) | ||||
|     * [database location](#database-location) | ||||
|     * [metadata from audio files](#metadata-from-audio-files) | ||||
|     * [file parser plugins](#file-parser-plugins) | ||||
|     * [complete examples](#complete-examples) | ||||
| @@ -50,8 +51,10 @@ turn your phone or raspi into a portable file server with resumable uploads/down | ||||
| * [sfx](#sfx) | ||||
|     * [sfx repack](#sfx-repack) | ||||
| * [install on android](#install-on-android) | ||||
| * [dev env setup](#dev-env-setup) | ||||
| * [how to release](#how-to-release) | ||||
| * [building](#building) | ||||
|     * [dev env setup](#dev-env-setup) | ||||
|     * [just the sfx](#just-the-sfx) | ||||
|     * [complete release](#complete-release) | ||||
| * [todo](#todo) | ||||
|  | ||||
|  | ||||
| @@ -61,6 +64,14 @@ download [copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/do | ||||
|  | ||||
| running the sfx without arguments (for example doubleclicking it on Windows) will give everyone full access to the current folder; see `-h` for help if you want accounts and volumes etc | ||||
|  | ||||
| some recommended options: | ||||
| * `-e2dsa` enables general file indexing, see [search configuration](#search-configuration) | ||||
| * `-e2ts` enables audio metadata indexing (needs either FFprobe or mutagen), see [optional dependencies](#optional-dependencies) | ||||
| * `-v /mnt/music:/music:r:afoo -a foo:bar` shares `/mnt/music` as `/music`, `r`eadable by anyone, with user `foo` as `a`dmin (read/write), password `bar` | ||||
|   * replace `:r:afoo` with `:rfoo` to only make the folder readable by `foo` and nobody else | ||||
|   * in addition to `r`ead and `a`dmin, `w`rite makes a folder write-only, so cannot list/access files in it | ||||
| * `--ls '**,*,ln,p,r'` to crash on startup if any of the volumes contain a symlink which point outside the volume, as that could give users unintended access | ||||
|  | ||||
| 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 better https) | ||||
| @@ -68,12 +79,16 @@ you may also want these, especially on servers: | ||||
|  | ||||
| ## notes | ||||
|  | ||||
| general: | ||||
| * paper-printing is affected by dark/light-mode! use lightmode for color, darkmode for grayscale | ||||
|   * because no browsers currently implement the media-query to do this properly orz | ||||
|  | ||||
| browser-specific: | ||||
| * iPhone/iPad: use Firefox to download files | ||||
| * Android-Chrome: increase "parallel uploads" for higher speed (android bug) | ||||
| * Android-Firefox: takes a while to select files (their fix for ☝️) | ||||
| * Desktop-Firefox: ~~may use gigabytes of RAM if your files are massive~~ *seems to be OK now* | ||||
| * paper-printing is affected by dark/light-mode! use lightmode for color, darkmode for grayscale | ||||
|   * because no browsers currently implement the media-query to do this properly orz | ||||
| * Desktop-Firefox: may stop you from deleting folders you've uploaded until you visit `about:memory` and click `Minimize memory usage` | ||||
|  | ||||
|  | ||||
| ## status | ||||
| @@ -96,11 +111,12 @@ summary: all planned features work! now please enjoy the bloatening | ||||
|   * ☑ FUSE client (read-only) | ||||
| * browser | ||||
|   * ☑ tree-view | ||||
|   * ☑ media player | ||||
|   * ☑ audio player | ||||
|   * ☑ thumbnails | ||||
|     * ☑ images using Pillow | ||||
|     * ☑ videos using FFmpeg | ||||
|     * ☑ cache eviction (max-age; maybe max-size eventually) | ||||
|   * ☑ image gallery | ||||
|   * ☑ SPA (browse while uploading) | ||||
|     * if you use the file-tree on the left only, not folders in the file list | ||||
| * server indexing | ||||
| @@ -117,16 +133,19 @@ summary: all planned features work! now please enjoy the bloatening | ||||
| * Windows: python 3.7 and older cannot read tags with ffprobe, so use mutagen or upgrade | ||||
| * Windows: python 2.7 cannot index non-ascii filenames with `-e2d` | ||||
| * Windows: python 2.7 cannot handle filenames with mojibake | ||||
| * MacOS: `--th-ff-jpg` may fix thumbnails using macports-FFmpeg | ||||
|  | ||||
| ## general bugs | ||||
|  | ||||
| * all volumes must exist / be available on startup; up2k (mtp especially) gets funky otherwise | ||||
| * cannot mount something at `/d1/d2/d3` unless `d2` exists inside `d1` | ||||
| * 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: folders cannot be accessed if the name ends with `.` | ||||
|   * python or windows bug | ||||
|  | ||||
| * 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 | ||||
|  | ||||
| @@ -157,7 +176,7 @@ the browser has the following hotkeys | ||||
|   * `0..9` jump to 10%..90% | ||||
|   * `U/O` skip 10sec back/forward | ||||
|   * `J/L` prev/next song | ||||
|     * `J` also starts playing the folder | ||||
|   * `M` play/pause (also starts playing the folder) | ||||
| * in the grid view: | ||||
|   * `S` toggle multiselect | ||||
|   * `A/D` zoom | ||||
| @@ -176,6 +195,8 @@ click `[-]` and `[+]` to adjust the size, and the `[a]` toggles if the tree shou | ||||
|  | ||||
| it does static images with Pillow and uses FFmpeg for video files, so you may want to `--no-thumb` or maybe just `--no-vthumb` depending on how destructive your users are | ||||
|  | ||||
| images named `folder.jpg` and `folder.png` become the thumbnail of the folder they're in | ||||
|  | ||||
|  | ||||
| ## zip downloads | ||||
|  | ||||
| @@ -292,9 +313,29 @@ the same arguments can be set as volume flags, in addition to `d2d` and `d2t` fo | ||||
| * `-v ~/music::r:cd2d` disables **all** indexing, even if any `-e2*` are on | ||||
| * `-v ~/music::r:cd2t` disables all `-e2t*` (tags), does not affect `-e2d*` | ||||
|  | ||||
| `e2tsr` is probably always overkill, since `e2ds`/`e2dsa` would pick up any file modifications and cause `e2ts` to reindex those | ||||
| note: | ||||
| * `e2tsr` is probably always overkill, since `e2ds`/`e2dsa` would pick up any file modifications and `e2ts` would then reindex those | ||||
| * the rescan button in the admin panel has no effect unless the volume has `-e2ds` or higher | ||||
|  | ||||
| the rescan button in the admin panel has no effect unless the volume has `-e2ds` or higher | ||||
| you can choose to only index filename/path/size/last-modified (and not the hash of the file contents) by setting `--no-hash` or the volume-flag `cdhash`, this has the following consequences: | ||||
| * initial indexing is way faster, especially when the volume is on a networked disk | ||||
| * makes it impossible to [file-search](#file-search) | ||||
| * if someone uploads the same file contents, the upload will not be detected as a dupe, so it will not get symlinked or rejected | ||||
|  | ||||
| if you set `--no-hash`, you can enable hashing for specific volumes using flag `cehash` | ||||
|  | ||||
|  | ||||
| ## database location | ||||
|  | ||||
| copyparty creates a subfolder named `.hist` inside each volume where it stores the database, thumbnails, and some other stuff | ||||
|  | ||||
| this can instead be kept in a single place using the `--hist` argument, or the `hist=` volume flag, or a mix of both: | ||||
| * `--hist ~/.cache/copyparty -v ~/music::r:chist=-` sets `~/.cache/copyparty` as the default place to put volume info, but `~/music` gets the regular `.hist` subfolder (`-` restores default behavior) | ||||
|  | ||||
| note: | ||||
| * markdown edits are always stored in a local `.hist` subdirectory | ||||
| * on windows the volflag path is cyglike, so `/c/temp` means `C:\temp` but use regular paths for `--hist` | ||||
|   * you can use cygpaths for volumes too, `-v C:\Users::r` and `-v /c/users::r` both work | ||||
|  | ||||
|  | ||||
| ## metadata from audio files | ||||
| @@ -401,7 +442,7 @@ quick summary of more eccentric web-browsers trying to view a directory index: | ||||
|  | ||||
| copyparty returns a truncated sha512sum of your PUT/POST as base64; you can generate the same checksum locally to verify uplaods: | ||||
|  | ||||
|     b512(){ printf "$((sha512sum||shasum -a512)|sed -E 's/ .*//;s/(..)/\\x\1/g')"|base64|head -c43;} | ||||
|     b512(){ printf "$((sha512sum||shasum -a512)|sed -E 's/ .*//;s/(..)/\\x\1/g')"|base64|tr '+/' '-_'|head -c44;} | ||||
|     b512 <movie.mkv | ||||
|  | ||||
|  | ||||
| @@ -496,18 +537,45 @@ echo $? | ||||
| after the initial setup, you can launch copyparty at any time by running `copyparty` anywhere in Termux | ||||
|  | ||||
|  | ||||
| # dev env setup | ||||
| # building | ||||
|  | ||||
| ## dev env setup | ||||
|  | ||||
| mostly optional; if you need a working env for vscode or similar | ||||
|  | ||||
| ```sh | ||||
| python3 -m venv .venv | ||||
| . .venv/bin/activate | ||||
| pip install jinja2  # mandatory deps | ||||
| pip install Pillow  # thumbnail deps | ||||
| pip install jinja2  # mandatory | ||||
| pip install mutagen  # audio metadata | ||||
| pip install Pillow pyheif-pillow-opener pillow-avif-plugin  # thumbnails | ||||
| pip install black bandit pylint flake8  # vscode tooling | ||||
| ``` | ||||
|  | ||||
|  | ||||
| # how to release | ||||
| ## just the sfx | ||||
|  | ||||
| unless you need to modify something in the web-dependencies, it's faster to grab those from a previous release: | ||||
|  | ||||
| ```sh | ||||
| rm -rf copyparty/web/deps | ||||
| curl -L https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py >x.py | ||||
| python3 x.py -h | ||||
| rm x.py | ||||
| mv /tmp/pe-copyparty/copyparty/web/deps/ copyparty/web/ | ||||
| ``` | ||||
|  | ||||
| then build the sfx using any of the following examples: | ||||
|  | ||||
| ```sh | ||||
| ./scripts/make-sfx.sh  # both python and sh editions | ||||
| ./scripts/make-sfx.sh no-sh gz  # just python with gzip | ||||
| ``` | ||||
|  | ||||
|  | ||||
| ## complete release | ||||
|  | ||||
| also builds the sfx so disregard the sfx section above | ||||
|  | ||||
| in the `scripts` folder: | ||||
|  | ||||
|   | ||||
| @@ -54,6 +54,12 @@ MACOS = platform.system() == "Darwin" | ||||
| info = log = dbg = None | ||||
|  | ||||
|  | ||||
| print("{} v{} @ {}".format( | ||||
|     platform.python_implementation(), | ||||
|     ".".join([str(x) for x in sys.version_info]), | ||||
|     sys.executable)) | ||||
|  | ||||
|  | ||||
| try: | ||||
|     from fuse import FUSE, FuseOSError, Operations | ||||
| except: | ||||
|   | ||||
| @@ -2,10 +2,13 @@ | ||||
|  | ||||
| import os | ||||
| import sys | ||||
| import time | ||||
| import shutil | ||||
| import sqlite3 | ||||
| import argparse | ||||
|  | ||||
| DB_VER = 3 | ||||
| DB_VER1 = 3 | ||||
| DB_VER2 = 4 | ||||
|  | ||||
|  | ||||
| def die(msg): | ||||
| @@ -45,18 +48,21 @@ def compare(n1, d1, n2, d2, verbose): | ||||
|     nt = next(d1.execute("select count(w) from up"))[0] | ||||
|     n = 0 | ||||
|     miss = 0 | ||||
|     for w, rd, fn in d1.execute("select w, rd, fn from up"): | ||||
|     for w1, rd, fn in d1.execute("select w, rd, fn from up"): | ||||
|         n += 1 | ||||
|         if n % 25_000 == 0: | ||||
|             m = f"\033[36mchecked {n:,} of {nt:,} files in {n1} against {n2}\033[0m" | ||||
|             print(m) | ||||
|  | ||||
|         q = "select w from up where substr(w,1,16) = ?" | ||||
|         hit = d2.execute(q, (w[:16],)).fetchone() | ||||
|         if rd.split("/", 1)[0] == ".hist": | ||||
|             continue | ||||
|  | ||||
|         q = "select w from up where rd = ? and fn = ?" | ||||
|         hit = d2.execute(q, (rd, fn)).fetchone() | ||||
|         if not hit: | ||||
|             miss += 1 | ||||
|             if verbose: | ||||
|                 print(f"file in {n1} missing in {n2}: [{w}] {rd}/{fn}") | ||||
|                 print(f"file in {n1} missing in {n2}: [{w1}] {rd}/{fn}") | ||||
|  | ||||
|     print(f" {miss} files in {n1} missing in {n2}\n") | ||||
|  | ||||
| @@ -64,15 +70,30 @@ def compare(n1, d1, n2, d2, verbose): | ||||
|     n = 0 | ||||
|     miss = {} | ||||
|     nmiss = 0 | ||||
|     for w, k, v in d1.execute("select * from mt"): | ||||
|     for w1, k, v in d1.execute("select * from mt"): | ||||
|  | ||||
|         n += 1 | ||||
|         if n % 100_000 == 0: | ||||
|             m = f"\033[36mchecked {n:,} of {nt:,} tags in {n1} against {n2}, so far {nmiss} missing tags\033[0m" | ||||
|             print(m) | ||||
|  | ||||
|         v2 = d2.execute("select v from mt where w = ? and +k = ?", (w, k)).fetchone() | ||||
|         if v2: | ||||
|             v2 = v2[0] | ||||
|         q = "select rd, fn from up where substr(w,1,16) = ?" | ||||
|         rd, fn = d1.execute(q, (w1,)).fetchone() | ||||
|         if rd.split("/", 1)[0] == ".hist": | ||||
|             continue | ||||
|  | ||||
|         q = "select substr(w,1,16) from up where rd = ? and fn = ?" | ||||
|         w2 = d2.execute(q, (rd, fn)).fetchone() | ||||
|         if w2: | ||||
|             w2 = w2[0] | ||||
|  | ||||
|         v2 = None | ||||
|         if w2: | ||||
|             v2 = d2.execute( | ||||
|                 "select v from mt where w = ? and +k = ?", (w2, k) | ||||
|             ).fetchone() | ||||
|             if v2: | ||||
|                 v2 = v2[0] | ||||
|  | ||||
|         # if v != v2 and v2 and k in [".bpm", "key"] and n2 == "src": | ||||
|         #    print(f"{w} [{rd}/{fn}] {k} = [{v}] / [{v2}]") | ||||
| @@ -99,9 +120,7 @@ def compare(n1, d1, n2, d2, verbose): | ||||
|                 miss[k] = 1 | ||||
|  | ||||
|             if verbose: | ||||
|                 q = "select rd, fn from up where substr(w,1,16) = ?" | ||||
|                 rd, fn = d1.execute(q, (w,)).fetchone() | ||||
|                 print(f"missing in {n2}: [{w}] [{rd}/{fn}] {k} = {v}") | ||||
|                 print(f"missing in {n2}: [{w1}] [{rd}/{fn}] {k} = {v}") | ||||
|  | ||||
|     for k, v in sorted(miss.items()): | ||||
|         if v: | ||||
| @@ -114,24 +133,35 @@ def copy_mtp(d1, d2, tag, rm): | ||||
|     nt = next(d1.execute("select count(w) from mt where k = ?", (tag,)))[0] | ||||
|     n = 0 | ||||
|     ndone = 0 | ||||
|     for w, k, v in d1.execute("select * from mt where k = ?", (tag,)): | ||||
|     for w1, k, v in d1.execute("select * from mt where k = ?", (tag,)): | ||||
|         n += 1 | ||||
|         if n % 25_000 == 0: | ||||
|             m = f"\033[36m{n:,} of {nt:,} tags checked, so far {ndone} copied\033[0m" | ||||
|             print(m) | ||||
|  | ||||
|         hit = d2.execute("select v from mt where w = ? and +k = ?", (w, k)).fetchone() | ||||
|         q = "select rd, fn from up where substr(w,1,16) = ?" | ||||
|         rd, fn = d1.execute(q, (w1,)).fetchone() | ||||
|         if rd.split("/", 1)[0] == ".hist": | ||||
|             continue | ||||
|  | ||||
|         q = "select substr(w,1,16) from up where rd = ? and fn = ?" | ||||
|         w2 = d2.execute(q, (rd, fn)).fetchone() | ||||
|         if not w2: | ||||
|             continue | ||||
|  | ||||
|         w2 = w2[0] | ||||
|         hit = d2.execute("select v from mt where w = ? and +k = ?", (w2, k)).fetchone() | ||||
|         if hit: | ||||
|             hit = hit[0] | ||||
|  | ||||
|         if hit != v: | ||||
|             ndone += 1 | ||||
|             if hit is not None: | ||||
|                 d2.execute("delete from mt where w = ? and +k = ?", (w, k)) | ||||
|                 d2.execute("delete from mt where w = ? and +k = ?", (w2, k)) | ||||
|  | ||||
|             d2.execute("insert into mt values (?,?,?)", (w, k, v)) | ||||
|             d2.execute("insert into mt values (?,?,?)", (w2, k, v)) | ||||
|             if rm: | ||||
|                 d2.execute("delete from mt where w = ? and +k = 't:mtp'", (w,)) | ||||
|                 d2.execute("delete from mt where w = ? and +k = 't:mtp'", (w2,)) | ||||
|  | ||||
|     d2.commit() | ||||
|     print(f"copied {ndone} {tag} tags over") | ||||
| @@ -140,7 +170,7 @@ def copy_mtp(d1, d2, tag, rm): | ||||
| def main(): | ||||
|     os.system("") | ||||
|     print() | ||||
|      | ||||
|  | ||||
|     ap = argparse.ArgumentParser() | ||||
|     ap.add_argument("db", help="database to work on") | ||||
|     ap.add_argument("-src", metavar="DB", type=str, help="database to copy from") | ||||
| @@ -168,6 +198,23 @@ def main(): | ||||
|     db = sqlite3.connect(ar.db) | ||||
|     ds = sqlite3.connect(ar.src) if ar.src else None | ||||
|  | ||||
|     # revert journals | ||||
|     for d, p in [[db, ar.db], [ds, ar.src]]: | ||||
|         if not d: | ||||
|             continue | ||||
|  | ||||
|         pj = "{}-journal".format(p) | ||||
|         if not os.path.exists(pj): | ||||
|             continue | ||||
|  | ||||
|         d.execute("create table foo (bar int)") | ||||
|         d.execute("drop table foo") | ||||
|  | ||||
|     if ar.copy: | ||||
|         db.close() | ||||
|         shutil.copy2(ar.db, "{}.bak.dbtool.{:x}".format(ar.db, int(time.time()))) | ||||
|         db = sqlite3.connect(ar.db) | ||||
|  | ||||
|     for d, n in [[ds, "src"], [db, "dst"]]: | ||||
|         if not d: | ||||
|             continue | ||||
| @@ -176,8 +223,8 @@ def main(): | ||||
|         if ver == "corrupt": | ||||
|             die("{} database appears to be corrupt, sorry") | ||||
|  | ||||
|         if ver != DB_VER: | ||||
|             m = f"{n} db is version {ver}, this tool only supports version {DB_VER}, please upgrade it with copyparty first" | ||||
|         if ver < DB_VER1 or ver > DB_VER2: | ||||
|             m = f"{n} db is version {ver}, this tool only supports versions between {DB_VER1} and {DB_VER2}, please upgrade it with copyparty first" | ||||
|             die(m) | ||||
|  | ||||
|     if ar.ls: | ||||
|   | ||||
| @@ -1,3 +1,8 @@ | ||||
| # when running copyparty behind a reverse-proxy, | ||||
| # make sure that copyparty allows at least as many clients as the proxy does, | ||||
| # so run copyparty with -nc 512 if your nginx has the default limits | ||||
| # (worker_processes 1, worker_connections 512) | ||||
|  | ||||
| upstream cpp { | ||||
| 	server 127.0.0.1:3923; | ||||
| 	keepalive 120; | ||||
|   | ||||
| @@ -23,7 +23,7 @@ from textwrap import dedent | ||||
| from .__init__ import E, WINDOWS, VT100, PY2 | ||||
| from .__version__ import S_VERSION, S_BUILD_DT, CODENAME | ||||
| from .svchub import SvcHub | ||||
| from .util import py_desc, align_tab, IMPLICATIONS | ||||
| from .util import py_desc, align_tab, IMPLICATIONS, alltrace | ||||
|  | ||||
| HAVE_SSL = True | ||||
| try: | ||||
| @@ -182,6 +182,16 @@ def sighandler(sig=None, frame=None): | ||||
|     print("\n".join(msg)) | ||||
|  | ||||
|  | ||||
| def stackmon(fp, ival): | ||||
|     ctr = 0 | ||||
|     while True: | ||||
|         ctr += 1 | ||||
|         time.sleep(ival) | ||||
|         st = "{}, {}\n{}".format(ctr, time.time(), alltrace()) | ||||
|         with open(fp, "wb") as f: | ||||
|             f.write(st.encode("utf-8", "replace")) | ||||
|  | ||||
|  | ||||
| def run_argparse(argv, formatter): | ||||
|     ap = argparse.ArgumentParser( | ||||
|         formatter_class=formatter, | ||||
| @@ -222,10 +232,6 @@ def run_argparse(argv, formatter): | ||||
|               "print,get" prints the data in the log and returns GET | ||||
|               (leave out the ",get" to return an error instead) | ||||
|  | ||||
|             --ciphers help = available ssl/tls ciphers, | ||||
|             --ssl-ver help = available ssl/tls versions, | ||||
|               default is what python considers safe, usually >= TLS1 | ||||
|              | ||||
|             values for --ls: | ||||
|               "USR" is a user to browse as; * is anonymous, ** is all users | ||||
|               "VOL" is a single volume to scan, default is * (all vols) | ||||
| @@ -238,29 +244,51 @@ def run_argparse(argv, formatter): | ||||
|               --ls '**'          # list all files which are possible to read | ||||
|               --ls '**,*,ln'     # check for dangerous symlinks | ||||
|               --ls '**,*,ln,p,r' # check, then start normally if safe | ||||
|             \033[0m | ||||
|             """ | ||||
|         ), | ||||
|     ) | ||||
|     # fmt: off | ||||
|     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 (comma-sep.)") | ||||
|     ap.add_argument("-p", metavar="PORT", type=str, default="3923", help="ports to bind (comma/range)") | ||||
|     ap.add_argument("-nc", metavar="NUM", type=int, default=64, help="max num clients") | ||||
|     ap.add_argument("-j", metavar="CORES", type=int, default=1, help="max num cpu cores") | ||||
|     ap.add_argument("-a", metavar="ACCT", type=str, action="append", help="add account") | ||||
|     ap.add_argument("-v", metavar="VOL", type=str, action="append", help="add volume") | ||||
|     ap.add_argument("-q", action="store_true", help="quiet") | ||||
|     ap.add_argument("-a", metavar="ACCT", type=str, action="append", help="add account, USER:PASS; example [ed:wark") | ||||
|     ap.add_argument("-v", metavar="VOL", type=str, action="append", help="add volume, SRC:DST:FLAG; example [.::r], [/mnt/nas/music:/music:r:aed") | ||||
|     ap.add_argument("-ed", action="store_true", help="enable ?dots") | ||||
|     ap.add_argument("-emp", action="store_true", help="enable markdown plugins") | ||||
|     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("-nih", action="store_true", help="no info hostname") | ||||
|     ap.add_argument("-nid", action="store_true", help="no info disk-usage") | ||||
|     ap.add_argument("--dotpart", action="store_true", help="dotfile incomplete uploads") | ||||
|     ap.add_argument("--no-zip", action="store_true", help="disable download as zip/tar") | ||||
|     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") | ||||
|     ap.add_argument("--urlform", metavar="MODE", type=str, default="print,get", help="how to handle url-forms; examples: [stash], [save,get]") | ||||
|  | ||||
|     ap2 = ap.add_argument_group('network options') | ||||
|     ap2.add_argument("-i", metavar="IP", type=str, default="0.0.0.0", help="ip to bind (comma-sep.)") | ||||
|     ap2.add_argument("-p", metavar="PORT", type=str, default="3923", help="ports to bind (comma/range)") | ||||
|     ap2.add_argument("--rproxy", metavar="DEPTH", type=int, default=1, help="which ip to keep; 0 = tcp, 1 = origin (first x-fwd), 2 = cloudflare, 3 = nginx, -1 = closest proxy") | ||||
|      | ||||
|     ap2 = ap.add_argument_group('SSL/TLS options') | ||||
|     ap2.add_argument("--http-only", action="store_true", help="disable ssl/tls") | ||||
|     ap2.add_argument("--https-only", action="store_true", help="disable plaintext") | ||||
|     ap2.add_argument("--ssl-ver", metavar="LIST", type=str, help="set allowed ssl/tls versions; [help] shows available versions; default is what your python version considers safe") | ||||
|     ap2.add_argument("--ciphers", metavar="LIST", help="set allowed ssl/tls ciphers; [help] shows available ciphers") | ||||
|     ap2.add_argument("--ssl-dbg", action="store_true", help="dump some tls info") | ||||
|     ap2.add_argument("--ssl-log", metavar="PATH", help="log master secrets") | ||||
|  | ||||
|     ap2 = ap.add_argument_group('opt-outs') | ||||
|     ap2.add_argument("-nw", action="store_true", help="disable writes (benchmark)") | ||||
|     ap2.add_argument("-nih", action="store_true", help="no info hostname") | ||||
|     ap2.add_argument("-nid", action="store_true", help="no info disk-usage") | ||||
|     ap2.add_argument("--no-zip", action="store_true", help="disable download as zip/tar") | ||||
|  | ||||
|     ap2 = ap.add_argument_group('safety options') | ||||
|     ap2.add_argument("--ls", metavar="U[,V[,F]]", help="scan all volumes; arguments USER,VOL,FLAGS; example [**,*,ln,p,r]") | ||||
|     ap2.add_argument("--salt", type=str, default="hunter2", help="up2k file-hash salt") | ||||
|  | ||||
|     ap2 = ap.add_argument_group('logging options') | ||||
|     ap2.add_argument("-q", action="store_true", help="quiet") | ||||
|     ap2.add_argument("--log-conn", action="store_true", help="print tcp-server msgs") | ||||
|     ap2.add_argument("--ihead", metavar="HEADER", action='append', help="dump incoming header") | ||||
|     ap2.add_argument("--lf-url", metavar="RE", type=str, default=r"^/\.cpr/|\?th=[wj]$", help="dont log URLs matching") | ||||
|  | ||||
|     ap2 = ap.add_argument_group('admin panel options') | ||||
|     ap2.add_argument("--no-rescan", action="store_true", help="disable ?scan (volume reindexing)") | ||||
| @@ -273,6 +301,7 @@ def run_argparse(argv, formatter): | ||||
|     ap2.add_argument("--th-no-crop", action="store_true", help="dynamic height; show full image") | ||||
|     ap2.add_argument("--th-no-jpg", action="store_true", help="disable jpg output") | ||||
|     ap2.add_argument("--th-no-webp", action="store_true", help="disable webp output") | ||||
|     ap2.add_argument("--th-ff-jpg", action="store_true", help="force jpg for video thumbs") | ||||
|     ap2.add_argument("--th-poke", metavar="SEC", type=int, default=300, help="activity labeling cooldown") | ||||
|     ap2.add_argument("--th-clean", metavar="SEC", type=int, default=43200, help="cleanup interval") | ||||
|     ap2.add_argument("--th-maxage", metavar="SEC", type=int, default=604800, help="max folder age") | ||||
| @@ -284,6 +313,8 @@ def run_argparse(argv, formatter): | ||||
|     ap2.add_argument("-e2t", action="store_true", help="enable metadata indexing") | ||||
|     ap2.add_argument("-e2ts", action="store_true", help="enable metadata scanner, sets -e2t") | ||||
|     ap2.add_argument("-e2tsr", action="store_true", help="rescan all metadata, sets -e2ts") | ||||
|     ap2.add_argument("--hist", metavar="PATH", type=str, help="where to store volume state") | ||||
|     ap2.add_argument("--no-hash", action="store_true", help="disable hashing during e2ds folder scans") | ||||
|     ap2.add_argument("--no-mutagen", action="store_true", help="use ffprobe for tags instead") | ||||
|     ap2.add_argument("--no-mtag-mt", action="store_true", help="disable tag-read parallelism") | ||||
|     ap2.add_argument("-mtm", metavar="M=t,t,t", action="append", type=str, help="add/replace metadata mapping") | ||||
| @@ -292,22 +323,14 @@ def run_argparse(argv, formatter): | ||||
|     ap2.add_argument("-mtp", metavar="M=[f,]bin", action="append", type=str, help="read tag M using bin") | ||||
|     ap2.add_argument("--srch-time", metavar="SEC", type=int, default=30, help="search deadline") | ||||
|  | ||||
|     ap2 = ap.add_argument_group('SSL/TLS options') | ||||
|     ap2.add_argument("--http-only", action="store_true", help="disable ssl/tls") | ||||
|     ap2.add_argument("--https-only", action="store_true", help="disable plaintext") | ||||
|     ap2.add_argument("--ssl-ver", metavar="LIST", type=str, help="ssl/tls versions to allow") | ||||
|     ap2.add_argument("--ciphers", metavar="LIST", help="set allowed ciphers") | ||||
|     ap2.add_argument("--ssl-dbg", action="store_true", help="dump some tls info") | ||||
|     ap2.add_argument("--ssl-log", metavar="PATH", help="log master secrets") | ||||
|     ap2 = ap.add_argument_group('appearance options') | ||||
|     ap2.add_argument("--css-browser", metavar="L", help="URL to additional CSS to include") | ||||
|  | ||||
|     ap2 = ap.add_argument_group('debug options') | ||||
|     ap2.add_argument("--ls", metavar="U[,V[,F]]", help="scan all volumes") | ||||
|     ap2.add_argument("--log-conn", action="store_true", help="print tcp-server msgs") | ||||
|     ap2.add_argument("--no-sendfile", action="store_true", help="disable sendfile") | ||||
|     ap2.add_argument("--no-scandir", action="store_true", help="disable scandir") | ||||
|     ap2.add_argument("--no-fastboot", action="store_true", help="wait for up2k indexing") | ||||
|     ap2.add_argument("--ihead", metavar="HEADER", action='append', help="dump incoming header") | ||||
|     ap2.add_argument("--lf-url", metavar="RE", type=str, default=r"^/\.cpr/|\?th=[wj]$", help="dont log URLs matching") | ||||
|     ap2.add_argument("--stackmon", metavar="P,S", help="write stacktrace to Path every S second") | ||||
|      | ||||
|     return ap.parse_args(args=argv[1:]) | ||||
|     # fmt: on | ||||
| @@ -347,6 +370,16 @@ def main(argv=None): | ||||
|     except AssertionError: | ||||
|         al = run_argparse(argv, Dodge11874) | ||||
|  | ||||
|     if al.stackmon: | ||||
|         fp, f = al.stackmon.rsplit(",", 1) | ||||
|         f = int(f) | ||||
|         t = threading.Thread( | ||||
|             target=stackmon, | ||||
|             args=(fp, f), | ||||
|         ) | ||||
|         t.daemon = True | ||||
|         t.start() | ||||
|  | ||||
|     # propagate implications | ||||
|     for k1, k2 in IMPLICATIONS: | ||||
|         if getattr(al, k1): | ||||
| @@ -377,6 +410,9 @@ def main(argv=None): | ||||
|             + "  (if you crash with codec errors then that is why)" | ||||
|         ) | ||||
|  | ||||
|     if WINDOWS and sys.version_info < (3, 6): | ||||
|         al.no_scandir = True | ||||
|  | ||||
|     # signal.signal(signal.SIGINT, sighandler) | ||||
|  | ||||
|     SvcHub(al).run() | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| # coding: utf-8 | ||||
|  | ||||
| VERSION = (0, 11, 8) | ||||
| VERSION = (0, 11, 20) | ||||
| CODENAME = "the grid" | ||||
| BUILD_DT = (2021, 6, 6) | ||||
| BUILD_DT = (2021, 6, 20) | ||||
|  | ||||
| S_VERSION = ".".join(map(str, VERSION)) | ||||
| S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT) | ||||
|   | ||||
| @@ -5,10 +5,12 @@ import re | ||||
| import os | ||||
| import sys | ||||
| import stat | ||||
| import base64 | ||||
| import hashlib | ||||
| import threading | ||||
|  | ||||
| from .__init__ import PY2, WINDOWS | ||||
| from .util import IMPLICATIONS, undot, Pebkac, fsdec, fsenc, statdir, nuprint | ||||
| from .__init__ import WINDOWS | ||||
| from .util import IMPLICATIONS, uncyg, undot, Pebkac, fsdec, fsenc, statdir, nuprint | ||||
|  | ||||
|  | ||||
| class VFS(object): | ||||
| @@ -22,7 +24,15 @@ class VFS(object): | ||||
|         self.uadm = uadm  # users who are regular admins | ||||
|         self.flags = flags  # config switches | ||||
|         self.nodes = {}  # child nodes | ||||
|         self.all_vols = {vpath: self}  # flattened recursive | ||||
|         self.histtab = None  # all realpath->histpath | ||||
|         self.dbv = None  # closest full/non-jump parent | ||||
|  | ||||
|         if realpath: | ||||
|             self.histpath = os.path.join(realpath, ".hist")  # db / thumbcache | ||||
|             self.all_vols = {vpath: self}  # flattened recursive | ||||
|         else: | ||||
|             self.histpath = None | ||||
|             self.all_vols = None | ||||
|  | ||||
|     def __repr__(self): | ||||
|         return "VFS({})".format( | ||||
| @@ -32,9 +42,12 @@ class VFS(object): | ||||
|             ) | ||||
|         ) | ||||
|  | ||||
|     def _trk(self, vol): | ||||
|         self.all_vols[vol.vpath] = vol | ||||
|         return vol | ||||
|     def get_all_vols(self, outdict): | ||||
|         if self.realpath: | ||||
|             outdict[self.vpath] = self | ||||
|  | ||||
|         for v in self.nodes.values(): | ||||
|             v.get_all_vols(outdict) | ||||
|  | ||||
|     def add(self, src, dst): | ||||
|         """get existing, or add new path to the vfs""" | ||||
| @@ -46,19 +59,19 @@ class VFS(object): | ||||
|             name, dst = dst.split("/", 1) | ||||
|             if name in self.nodes: | ||||
|                 # exists; do not manipulate permissions | ||||
|                 return self._trk(self.nodes[name].add(src, dst)) | ||||
|                 return self.nodes[name].add(src, dst) | ||||
|  | ||||
|             vn = VFS( | ||||
|                 "{}/{}".format(self.realpath, name), | ||||
|                 os.path.join(self.realpath, name) if self.realpath else None, | ||||
|                 "{}/{}".format(self.vpath, name).lstrip("/"), | ||||
|                 self.uread, | ||||
|                 self.uwrite, | ||||
|                 self.uadm, | ||||
|                 self.flags, | ||||
|                 self._copy_flags(name), | ||||
|             ) | ||||
|             self._trk(vn) | ||||
|             vn.dbv = self.dbv or self | ||||
|             self.nodes[name] = vn | ||||
|             return self._trk(vn.add(src, dst)) | ||||
|             return vn.add(src, dst) | ||||
|  | ||||
|         if dst in self.nodes: | ||||
|             # leaf exists; return as-is | ||||
| @@ -67,8 +80,26 @@ class VFS(object): | ||||
|         # leaf does not exist; create and keep permissions blank | ||||
|         vp = "{}/{}".format(self.vpath, dst).lstrip("/") | ||||
|         vn = VFS(src, vp) | ||||
|         vn.dbv = self.dbv or self | ||||
|         self.nodes[dst] = vn | ||||
|         return self._trk(vn) | ||||
|         return vn | ||||
|  | ||||
|     def _copy_flags(self, name): | ||||
|         flags = {k: v for k, v in self.flags.items()} | ||||
|         hist = flags.get("hist") | ||||
|         if hist and hist != "-": | ||||
|             flags["hist"] = "{}/{}".format(hist.rstrip("/"), name) | ||||
|  | ||||
|         return flags | ||||
|  | ||||
|     def bubble_flags(self): | ||||
|         if self.dbv: | ||||
|             for k, v in self.dbv.flags.items(): | ||||
|                 if k not in ["hist"]: | ||||
|                     self.flags[k] = v | ||||
|  | ||||
|         for v in self.nodes.values(): | ||||
|             v.bubble_flags() | ||||
|  | ||||
|     def _find(self, vpath): | ||||
|         """return [vfs,remainder]""" | ||||
| @@ -96,6 +127,7 @@ class VFS(object): | ||||
|         ] | ||||
|  | ||||
|     def get(self, vpath, uname, will_read, will_write): | ||||
|         # type: (str, str, bool, bool) -> tuple[VFS, str] | ||||
|         """returns [vfsnode,fs_remainder] if user has the requested permissions""" | ||||
|         vn, rem = self._find(vpath) | ||||
|  | ||||
| @@ -107,6 +139,15 @@ class VFS(object): | ||||
|  | ||||
|         return vn, rem | ||||
|  | ||||
|     def get_dbv(self, vrem): | ||||
|         dbv = self.dbv | ||||
|         if not dbv: | ||||
|             return self, vrem | ||||
|  | ||||
|         vrem = [self.vpath[len(dbv.vpath) + 1 :], vrem] | ||||
|         vrem = "/".join([x for x in vrem if x]) | ||||
|         return dbv, vrem | ||||
|  | ||||
|     def canonical(self, rem): | ||||
|         """returns the canonical path (fully-resolved absolute fs path)""" | ||||
|         rp = self.realpath | ||||
| @@ -136,6 +177,7 @@ class VFS(object): | ||||
|             return os.path.realpath(rp) | ||||
|  | ||||
|     def ls(self, rem, uname, scandir, incl_wo=False, lstat=False): | ||||
|         # type: (str, str, bool, bool, bool) -> tuple[str, str, dict[str, VFS]] | ||||
|         """return user-readable [fsdir,real,virt] items at vpath""" | ||||
|         virt_vis = {}  # nodes readable by user | ||||
|         abspath = self.canonical(rem) | ||||
| @@ -156,13 +198,21 @@ class VFS(object): | ||||
|  | ||||
|         return [abspath, real, virt_vis] | ||||
|  | ||||
|     def walk(self, rel, rem, uname, dots, scandir, lstat=False): | ||||
|     def walk(self, rel, rem, seen, uname, dots, scandir, lstat): | ||||
|         """ | ||||
|         recursively yields from ./rem; | ||||
|         rel is a unix-style user-defined vpath (not vfs-related) | ||||
|         """ | ||||
|  | ||||
|         fsroot, vfs_ls, vfs_virt = self.ls(rem, uname, scandir, False, lstat) | ||||
|         fsroot, vfs_ls, vfs_virt = self.ls( | ||||
|             rem, uname, scandir, incl_wo=False, lstat=lstat | ||||
|         ) | ||||
|  | ||||
|         if seen and not fsroot.startswith(seen[-1]) and fsroot in seen: | ||||
|             print("bailing from symlink loop,\n  {}\n  {}".format(seen[-1], fsroot)) | ||||
|             return | ||||
|  | ||||
|         seen = seen[:] + [fsroot] | ||||
|         rfiles = [x for x in vfs_ls if not stat.S_ISDIR(x[1].st_mode)] | ||||
|         rdirs = [x for x in vfs_ls if stat.S_ISDIR(x[1].st_mode)] | ||||
|  | ||||
| @@ -177,7 +227,7 @@ class VFS(object): | ||||
|  | ||||
|             wrel = (rel + "/" + rdir).lstrip("/") | ||||
|             wrem = (rem + "/" + rdir).lstrip("/") | ||||
|             for x in self.walk(wrel, wrem, uname, scandir, lstat): | ||||
|             for x in self.walk(wrel, wrem, seen, uname, dots, scandir, lstat): | ||||
|                 yield x | ||||
|  | ||||
|         for n, vfs in sorted(vfs_virt.items()): | ||||
| @@ -185,14 +235,16 @@ class VFS(object): | ||||
|                 continue | ||||
|  | ||||
|             wrel = (rel + "/" + n).lstrip("/") | ||||
|             for x in vfs.walk(wrel, "", uname, scandir, lstat): | ||||
|             for x in vfs.walk(wrel, "", seen, uname, dots, scandir, lstat): | ||||
|                 yield x | ||||
|  | ||||
|     def zipgen(self, vrem, flt, uname, dots, scandir): | ||||
|         if flt: | ||||
|             flt = {k: True for k in flt} | ||||
|  | ||||
|         for vpath, apath, files, rd, vd in self.walk("", vrem, uname, dots, scandir): | ||||
|         for vpath, apath, files, rd, vd in self.walk( | ||||
|             "", vrem, [], uname, dots, scandir, False | ||||
|         ): | ||||
|             if flt: | ||||
|                 files = [x for x in files if x[0] in flt] | ||||
|  | ||||
| @@ -261,7 +313,8 @@ class AuthSrv(object): | ||||
|         self.reload() | ||||
|  | ||||
|     def log(self, msg, c=0): | ||||
|         self.log_func("auth", msg, c) | ||||
|         if self.log_func: | ||||
|             self.log_func("auth", msg, c) | ||||
|  | ||||
|     def laggy_iter(self, iterable): | ||||
|         """returns [value,isFinalValue]""" | ||||
| @@ -386,6 +439,9 @@ class AuthSrv(object): | ||||
|                     raise Exception("invalid -v argument: [{}]".format(v_str)) | ||||
|  | ||||
|                 src, dst, perms = m.groups() | ||||
|                 if WINDOWS: | ||||
|                     src = uncyg(src) | ||||
|  | ||||
|                 # print("\n".join([src, dst, perms])) | ||||
|                 src = fsdec(os.path.abspath(fsenc(src))) | ||||
|                 dst = dst.strip("/") | ||||
| @@ -413,12 +469,23 @@ class AuthSrv(object): | ||||
|                         print(m.format(cfg_fn, self.line_ctr)) | ||||
|                         raise | ||||
|  | ||||
|         # case-insensitive; normalize | ||||
|         if WINDOWS: | ||||
|             cased = {} | ||||
|             for k, v in mount.items(): | ||||
|                 try: | ||||
|                     cased[k] = fsdec(os.path.realpath(fsenc(v))) | ||||
|                 except: | ||||
|                     cased[k] = v | ||||
|  | ||||
|             mount = cased | ||||
|  | ||||
|         if not mount: | ||||
|             # -h says our defaults are CWD at root and read/write for everyone | ||||
|             vfs = VFS(os.path.abspath("."), "", ["*"], ["*"]) | ||||
|         elif "" not in mount: | ||||
|             # there's volumes but no root; make root inaccessible | ||||
|             vfs = VFS(os.path.abspath("."), "") | ||||
|             vfs = VFS(None, "") | ||||
|             vfs.flags["d2d"] = True | ||||
|  | ||||
|         maxdepth = 0 | ||||
| @@ -439,6 +506,10 @@ class AuthSrv(object): | ||||
|             v.uwrite = mwrite[dst] | ||||
|             v.uadm = madm[dst] | ||||
|             v.flags = mflags[dst] | ||||
|             v.dbv = None | ||||
|  | ||||
|         vfs.all_vols = {} | ||||
|         vfs.get_all_vols(vfs.all_vols) | ||||
|  | ||||
|         missing_users = {} | ||||
|         for d in [mread, mwrite]: | ||||
| @@ -455,6 +526,67 @@ class AuthSrv(object): | ||||
|             ) | ||||
|             raise Exception("invalid config") | ||||
|  | ||||
|         promote = [] | ||||
|         demote = [] | ||||
|         for vol in vfs.all_vols.values(): | ||||
|             hid = hashlib.sha512(fsenc(vol.realpath)).digest() | ||||
|             hid = base64.b32encode(hid).decode("ascii").lower() | ||||
|             vflag = vol.flags.get("hist") | ||||
|             if vflag == "-": | ||||
|                 pass | ||||
|             elif vflag: | ||||
|                 vol.histpath = uncyg(vflag) if WINDOWS else vflag | ||||
|             elif self.args.hist: | ||||
|                 for nch in range(len(hid)): | ||||
|                     hpath = os.path.join(self.args.hist, hid[: nch + 1]) | ||||
|                     try: | ||||
|                         os.makedirs(hpath) | ||||
|                     except: | ||||
|                         pass | ||||
|  | ||||
|                     powner = os.path.join(hpath, "owner.txt") | ||||
|                     try: | ||||
|                         with open(powner, "rb") as f: | ||||
|                             owner = f.read().rstrip() | ||||
|                     except: | ||||
|                         owner = None | ||||
|  | ||||
|                     me = fsenc(vol.realpath).rstrip() | ||||
|                     if owner not in [None, me]: | ||||
|                         continue | ||||
|  | ||||
|                     if owner is None: | ||||
|                         with open(powner, "wb") as f: | ||||
|                             f.write(me) | ||||
|  | ||||
|                     vol.histpath = hpath | ||||
|                     break | ||||
|  | ||||
|             vol.histpath = os.path.realpath(vol.histpath) | ||||
|             if vol.dbv: | ||||
|                 if os.path.exists(os.path.join(vol.histpath, "up2k.db")): | ||||
|                     promote.append(vol) | ||||
|                     vol.dbv = None | ||||
|                 else: | ||||
|                     demote.append(vol) | ||||
|  | ||||
|         # discard jump-vols | ||||
|         for v in demote: | ||||
|             vfs.all_vols.pop(v.vpath) | ||||
|  | ||||
|         if promote: | ||||
|             msg = [ | ||||
|                 "\n  the following jump-volumes were generated to assist the vfs.\n  As they contain a database (probably from v0.11.11 or older),\n  they are promoted to full volumes:" | ||||
|             ] | ||||
|             for vol in promote: | ||||
|                 msg.append( | ||||
|                     "  /{}  ({})  ({})".format(vol.vpath, vol.realpath, vol.histpath) | ||||
|                 ) | ||||
|  | ||||
|             self.log("\n\n".join(msg) + "\n", c=3) | ||||
|  | ||||
|         vfs.histtab = {v.realpath: v.histpath for v in vfs.all_vols.values()} | ||||
|  | ||||
|         all_mte = {} | ||||
|         errors = False | ||||
|         for vol in vfs.all_vols.values(): | ||||
| @@ -464,6 +596,10 @@ class AuthSrv(object): | ||||
|             if self.args.e2d or "e2ds" in vol.flags: | ||||
|                 vol.flags["e2d"] = True | ||||
|  | ||||
|             if self.args.no_hash: | ||||
|                 if "ehash" not in vol.flags: | ||||
|                     vol.flags["dhash"] = True | ||||
|  | ||||
|             for k in ["e2t", "e2ts", "e2tsr"]: | ||||
|                 if getattr(self.args, k): | ||||
|                     vol.flags[k] = True | ||||
| @@ -541,6 +677,8 @@ class AuthSrv(object): | ||||
|         if errors: | ||||
|             sys.exit(1) | ||||
|  | ||||
|         vfs.bubble_flags() | ||||
|  | ||||
|         try: | ||||
|             v, _ = vfs.get("/", "*", False, True) | ||||
|             if self.warn_anonwrite and os.getcwd() == v.realpath: | ||||
| @@ -616,13 +754,13 @@ class AuthSrv(object): | ||||
|                     continue | ||||
|  | ||||
|                 atop = vn.realpath | ||||
|                 g = vn.walk("", "", u, True, not self.args.no_scandir, lstat=False) | ||||
|                 g = vn.walk("", "", [], u, True, not self.args.no_scandir, False) | ||||
|                 for vpath, apath, files, _, _ in g: | ||||
|                     fnames = [n[0] for n in files] | ||||
|                     vpaths = [vpath + "/" + n for n in fnames] if vpath else fnames | ||||
|                     vpaths = [vtop + x for x in vpaths] | ||||
|                     apaths = [os.path.join(apath, n) for n in fnames] | ||||
|                     files = list(zip(vpaths, apaths)) | ||||
|                     files = [[vpath + "/", apath + os.sep]] + list(zip(vpaths, apaths)) | ||||
|  | ||||
|                     if flag_ln: | ||||
|                         files = [x for x in files if not x[1].startswith(atop + os.sep)] | ||||
|   | ||||
| @@ -44,7 +44,9 @@ class BrokerMp(object): | ||||
|             proc.clients = {} | ||||
|             proc.workload = 0 | ||||
|  | ||||
|             thr = threading.Thread(target=self.collector, args=(proc,)) | ||||
|             thr = threading.Thread( | ||||
|                 target=self.collector, args=(proc,), name="mp-collector" | ||||
|             ) | ||||
|             thr.daemon = True | ||||
|             thr.start() | ||||
|  | ||||
| @@ -52,14 +54,19 @@ class BrokerMp(object): | ||||
|             proc.start() | ||||
|  | ||||
|         if not self.args.q: | ||||
|             thr = threading.Thread(target=self.debug_load_balancer) | ||||
|             thr = threading.Thread( | ||||
|                 target=self.debug_load_balancer, name="mp-dbg-loadbalancer" | ||||
|             ) | ||||
|             thr.daemon = True | ||||
|             thr.start() | ||||
|  | ||||
|     def shutdown(self): | ||||
|         self.log("broker", "shutting down") | ||||
|         for proc in self.procs: | ||||
|             thr = threading.Thread(target=proc.q_pend.put([0, "shutdown", []])) | ||||
|         for n, proc in enumerate(self.procs): | ||||
|             thr = threading.Thread( | ||||
|                 target=proc.q_pend.put([0, "shutdown", []]), | ||||
|                 name="mp-shutdown-{}-{}".format(n, len(self.procs)), | ||||
|             ) | ||||
|             thr.start() | ||||
|  | ||||
|         with self.mutex: | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| # coding: utf-8 | ||||
| from __future__ import print_function, unicode_literals | ||||
| from copyparty.authsrv import AuthSrv | ||||
|  | ||||
| import sys | ||||
| import time | ||||
| @@ -27,20 +28,23 @@ class MpWorker(object): | ||||
|         self.retpend = {} | ||||
|         self.retpend_mutex = threading.Lock() | ||||
|         self.mutex = threading.Lock() | ||||
|         self.workload_thr_active = False | ||||
|         self.workload_thr_alive = False | ||||
|  | ||||
|         # we inherited signal_handler from parent, | ||||
|         # replace it with something harmless | ||||
|         if not FAKE_MP: | ||||
|             signal.signal(signal.SIGINT, self.signal_handler) | ||||
|  | ||||
|         # starting to look like a good idea | ||||
|         self.asrv = AuthSrv(args, None, False) | ||||
|  | ||||
|         # instantiate all services here (TODO: inheritance?) | ||||
|         self.httpsrv = HttpSrv(self) | ||||
|         self.httpsrv = HttpSrv(self, True) | ||||
|         self.httpsrv.disconnect_func = self.httpdrop | ||||
|  | ||||
|         # on winxp and some other platforms, | ||||
|         # use thr.join() to block all signals | ||||
|         thr = threading.Thread(target=self.main) | ||||
|         thr = threading.Thread(target=self.main, name="mpw-main") | ||||
|         thr.daemon = True | ||||
|         thr.start() | ||||
|         thr.join() | ||||
| @@ -64,6 +68,7 @@ class MpWorker(object): | ||||
|  | ||||
|             # self.logw("work: [{}]".format(d[0])) | ||||
|             if dest == "shutdown": | ||||
|                 self.httpsrv.shutdown() | ||||
|                 self.logw("ok bye") | ||||
|                 sys.exit(0) | ||||
|                 return | ||||
| @@ -75,13 +80,15 @@ class MpWorker(object): | ||||
|  | ||||
|                 if self.args.log_conn: | ||||
|                     self.log("%s %s" % addr, "|%sC-qpop" % ("-" * 4,), c="1;30") | ||||
|                  | ||||
|  | ||||
|                 self.httpsrv.accept(sck, addr) | ||||
|  | ||||
|                 with self.mutex: | ||||
|                     if not self.workload_thr_active: | ||||
|                     if not self.workload_thr_alive: | ||||
|                         self.workload_thr_alive = True | ||||
|                         thr = threading.Thread(target=self.thr_workload) | ||||
|                         thr = threading.Thread( | ||||
|                             target=self.thr_workload, name="mpw-workload" | ||||
|                         ) | ||||
|                         thr.daemon = True | ||||
|                         thr.start() | ||||
|  | ||||
|   | ||||
| @@ -3,6 +3,7 @@ from __future__ import print_function, unicode_literals | ||||
|  | ||||
| import threading | ||||
|  | ||||
| from .authsrv import AuthSrv | ||||
| from .httpsrv import HttpSrv | ||||
| from .broker_util import ExceptionalQueue, try_exec | ||||
|  | ||||
| @@ -14,6 +15,7 @@ class BrokerThr(object): | ||||
|         self.hub = hub | ||||
|         self.log = hub.log | ||||
|         self.args = hub.args | ||||
|         self.asrv = hub.asrv | ||||
|  | ||||
|         self.mutex = threading.Lock() | ||||
|  | ||||
| @@ -23,6 +25,7 @@ class BrokerThr(object): | ||||
|  | ||||
|     def shutdown(self): | ||||
|         # self.log("broker", "shutting down") | ||||
|         self.httpsrv.shutdown() | ||||
|         pass | ||||
|  | ||||
|     def put(self, want_retval, dest, *args): | ||||
|   | ||||
| @@ -16,6 +16,7 @@ import calendar | ||||
|  | ||||
| from .__init__ import E, PY2, WINDOWS, ANYWIN | ||||
| from .util import *  # noqa  # pylint: disable=unused-wildcard-import | ||||
| from .authsrv import AuthSrv | ||||
| from .szip import StreamZip | ||||
| from .star import StreamTar | ||||
|  | ||||
| @@ -35,12 +36,13 @@ class HttpCli(object): | ||||
|     def __init__(self, conn): | ||||
|         self.t0 = time.time() | ||||
|         self.conn = conn | ||||
|         self.s = conn.s | ||||
|         self.sr = conn.sr | ||||
|         self.s = conn.s  # type: socket | ||||
|         self.sr = conn.sr  # type: Unrecv | ||||
|         self.ip = conn.addr[0] | ||||
|         self.addr = conn.addr | ||||
|         self.addr = conn.addr  # type: tuple[str, int] | ||||
|         self.args = conn.args | ||||
|         self.auth = conn.auth | ||||
|         self.is_mp = conn.is_mp | ||||
|         self.asrv = conn.asrv  # type: AuthSrv | ||||
|         self.ico = conn.ico | ||||
|         self.thumbcli = conn.thumbcli | ||||
|         self.log_func = conn.log_func | ||||
| @@ -102,10 +104,21 @@ class HttpCli(object): | ||||
|         v = self.headers.get("connection", "").lower() | ||||
|         self.keepalive = not v.startswith("close") and self.http_ver != "HTTP/1.0" | ||||
|  | ||||
|         v = self.headers.get("x-forwarded-for", None) | ||||
|         if v is not None and self.conn.addr[0] in ["127.0.0.1", "::1"]: | ||||
|             self.ip = v.split(",")[0] | ||||
|             self.log_src = self.conn.set_rproxy(self.ip) | ||||
|         n = self.args.rproxy | ||||
|         if n: | ||||
|             v = self.headers.get("x-forwarded-for") | ||||
|             if v and self.conn.addr[0] in ["127.0.0.1", "::1"]: | ||||
|                 if n > 0: | ||||
|                     n -= 1 | ||||
|  | ||||
|                 vs = v.split(",") | ||||
|                 try: | ||||
|                     self.ip = vs[n].strip() | ||||
|                 except: | ||||
|                     self.ip = vs[0].strip() | ||||
|                     self.log("rproxy={} oob x-fwd {}".format(self.args.rproxy, v), c=3) | ||||
|  | ||||
|                 self.log_src = self.conn.set_rproxy(self.ip) | ||||
|  | ||||
|         if self.args.ihead: | ||||
|             keys = self.args.ihead | ||||
| @@ -152,9 +165,9 @@ class HttpCli(object): | ||||
|         self.vpath = unquotep(vpath) | ||||
|  | ||||
|         pwd = uparam.get("pw") | ||||
|         self.uname = self.auth.iuser.get(pwd, "*") | ||||
|         self.uname = self.asrv.iuser.get(pwd, "*") | ||||
|         self.rvol, self.wvol, self.avol = [[], [], []] | ||||
|         self.auth.vfs.user_tree(self.uname, self.rvol, self.wvol, self.avol) | ||||
|         self.asrv.vfs.user_tree(self.uname, self.rvol, self.wvol, self.avol) | ||||
|  | ||||
|         ua = self.headers.get("user-agent", "") | ||||
|         self.is_rclone = ua.startswith("rclone/") | ||||
| @@ -243,10 +256,11 @@ class HttpCli(object): | ||||
|         if self.is_rclone: | ||||
|             return "" | ||||
|  | ||||
|         cmap = {"pw": "cppwd"} | ||||
|         kv = { | ||||
|             k: v | ||||
|             for k, v in self.uparam.items() | ||||
|             if k not in rm and self.cookies.get(k) != v | ||||
|             if k not in rm and self.cookies.get(cmap.get(k, k)) != v | ||||
|         } | ||||
|         kv.update(add) | ||||
|         if not kv: | ||||
| @@ -256,7 +270,14 @@ class HttpCli(object): | ||||
|         return "?" + "&".join(r) | ||||
|  | ||||
|     def redirect( | ||||
|         self, vpath, suf="", msg="aight", flavor="go to", click=True, use302=False | ||||
|         self, | ||||
|         vpath, | ||||
|         suf="", | ||||
|         msg="aight", | ||||
|         flavor="go to", | ||||
|         click=True, | ||||
|         status=200, | ||||
|         use302=False, | ||||
|     ): | ||||
|         html = self.j2( | ||||
|             "msg", | ||||
| @@ -271,7 +292,7 @@ class HttpCli(object): | ||||
|             h = {"Location": "/" + vpath, "Cache-Control": "no-cache"} | ||||
|             self.reply(html, status=302, headers=h) | ||||
|         else: | ||||
|             self.reply(html) | ||||
|             self.reply(html, status=status) | ||||
|  | ||||
|     def handle_get(self): | ||||
|         if self.do_log: | ||||
| @@ -312,9 +333,7 @@ class HttpCli(object): | ||||
|                     self.redirect(vpath, flavor="redirecting to", use302=True) | ||||
|                     return True | ||||
|  | ||||
|         self.readable, self.writable = self.conn.auth.vfs.can_access( | ||||
|             self.vpath, self.uname | ||||
|         ) | ||||
|         self.readable, self.writable = self.asrv.vfs.can_access(self.vpath, self.uname) | ||||
|         if not self.readable and not self.writable: | ||||
|             if self.vpath: | ||||
|                 self.log("inaccessible: [{}]".format(self.vpath)) | ||||
| @@ -431,7 +450,7 @@ class HttpCli(object): | ||||
|  | ||||
|     def dump_to_file(self): | ||||
|         reader, remains = self.get_body_reader() | ||||
|         vfs, rem = self.conn.auth.vfs.get(self.vpath, self.uname, False, True) | ||||
|         vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True) | ||||
|         fdir = os.path.join(vfs.realpath, rem) | ||||
|  | ||||
|         addr = self.ip.replace(":", ".") | ||||
| @@ -441,8 +460,10 @@ class HttpCli(object): | ||||
|         with open(fsenc(path), "wb", 512 * 1024) as f: | ||||
|             post_sz, _, sha_b64 = hashcopy(self.conn, reader, f) | ||||
|  | ||||
|         vfs, vrem = vfs.get_dbv(rem) | ||||
|  | ||||
|         self.conn.hsrv.broker.put( | ||||
|             False, "up2k.hash_file", vfs.realpath, vfs.flags, rem, fn | ||||
|             False, "up2k.hash_file", vfs.realpath, vfs.flags, vrem, fn | ||||
|         ) | ||||
|  | ||||
|         return post_sz, sha_b64, remains, path | ||||
| @@ -498,7 +519,7 @@ class HttpCli(object): | ||||
|         if v is None: | ||||
|             raise Pebkac(422, "need zip or tar keyword") | ||||
|  | ||||
|         vn, rem = self.auth.vfs.get(self.vpath, self.uname, True, False) | ||||
|         vn, rem = self.asrv.vfs.get(self.vpath, self.uname, True, False) | ||||
|         items = self.parser.require("files", 1024 * 1024) | ||||
|         if not items: | ||||
|             raise Pebkac(422, "need files list") | ||||
| @@ -506,6 +527,7 @@ class HttpCli(object): | ||||
|         items = items.replace("\r", "").split("\n") | ||||
|         items = [unquotep(x) for x in items if items] | ||||
|  | ||||
|         self.parser.drop() | ||||
|         return self.tx_zip(k, v, vn, rem, items, self.args.ed) | ||||
|  | ||||
|     def handle_post_json(self): | ||||
| @@ -547,22 +569,32 @@ class HttpCli(object): | ||||
|             self.vpath = "/".join([self.vpath, sub]).strip("/") | ||||
|             body["name"] = name | ||||
|  | ||||
|         vfs, rem = self.conn.auth.vfs.get(self.vpath, self.uname, False, True) | ||||
|         vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True) | ||||
|         dbv, vrem = vfs.get_dbv(rem) | ||||
|  | ||||
|         body["vtop"] = vfs.vpath | ||||
|         body["ptop"] = vfs.realpath | ||||
|         body["prel"] = rem | ||||
|         body["vtop"] = dbv.vpath | ||||
|         body["ptop"] = dbv.realpath | ||||
|         body["prel"] = vrem | ||||
|         body["addr"] = self.ip | ||||
|         body["vcfg"] = vfs.flags | ||||
|         body["vcfg"] = dbv.flags | ||||
|  | ||||
|         if sub: | ||||
|             try: | ||||
|                 dst = os.path.join(vfs.realpath, rem) | ||||
|                 os.makedirs(fsenc(dst)) | ||||
|             except: | ||||
|                 if not os.path.isdir(fsenc(dst)): | ||||
|                     os.makedirs(fsenc(dst)) | ||||
|             except OSError as ex: | ||||
|                 self.log("makedirs failed [{}]".format(dst)) | ||||
|                 if ex.errno == 13: | ||||
|                     raise Pebkac(500, "the server OS denied write-access") | ||||
|  | ||||
|                 if ex.errno == 17: | ||||
|                     raise Pebkac(400, "some file got your folder name") | ||||
|  | ||||
|                 raise Pebkac(500, min_ex()) | ||||
|             except: | ||||
|                 raise Pebkac(500, min_ex()) | ||||
|  | ||||
|         x = self.conn.hsrv.broker.put(True, "up2k.handle_json", body) | ||||
|         ret = x.get() | ||||
|         if sub: | ||||
| @@ -575,8 +607,14 @@ class HttpCli(object): | ||||
|  | ||||
|     def handle_search(self, body): | ||||
|         vols = [] | ||||
|         seen = {} | ||||
|         for vtop in self.rvol: | ||||
|             vfs, _ = self.conn.auth.vfs.get(vtop, self.uname, True, False) | ||||
|             vfs, _ = self.asrv.vfs.get(vtop, self.uname, True, False) | ||||
|             vfs = vfs.dbv or vfs | ||||
|             if vfs in seen: | ||||
|                 continue | ||||
|  | ||||
|             seen[vfs] = True | ||||
|             vols.append([vfs.vpath, vfs.realpath, vfs.flags]) | ||||
|  | ||||
|         idx = self.conn.get_u2idx() | ||||
| @@ -632,8 +670,8 @@ class HttpCli(object): | ||||
|         except KeyError: | ||||
|             raise Pebkac(400, "need hash and wark headers for binary POST") | ||||
|  | ||||
|         vfs, _ = self.conn.auth.vfs.get(self.vpath, self.uname, False, True) | ||||
|         ptop = vfs.realpath | ||||
|         vfs, _ = self.asrv.vfs.get(self.vpath, self.uname, False, True) | ||||
|         ptop = (vfs.dbv or vfs).realpath | ||||
|  | ||||
|         x = self.conn.hsrv.broker.put(True, "up2k.handle_chunk", ptop, wark, chash) | ||||
|         response = x.get() | ||||
| @@ -705,7 +743,7 @@ class HttpCli(object): | ||||
|         pwd = self.parser.require("cppwd", 64) | ||||
|         self.parser.drop() | ||||
|  | ||||
|         if pwd in self.auth.iuser: | ||||
|         if pwd in self.asrv.iuser: | ||||
|             msg = "login ok" | ||||
|             dt = datetime.utcfromtimestamp(time.time() + 60 * 60 * 24 * 365) | ||||
|             exp = dt.strftime("%a, %d %b %Y %H:%M:%S GMT") | ||||
| @@ -724,7 +762,7 @@ class HttpCli(object): | ||||
|         self.parser.drop() | ||||
|  | ||||
|         nullwrite = self.args.nw | ||||
|         vfs, rem = self.conn.auth.vfs.get(self.vpath, self.uname, False, True) | ||||
|         vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True) | ||||
|         self._assert_safe_rem(rem) | ||||
|  | ||||
|         sanitized = sanitize_fn(new_dir) | ||||
| @@ -741,8 +779,13 @@ class HttpCli(object): | ||||
|  | ||||
|             try: | ||||
|                 os.mkdir(fsenc(fn)) | ||||
|             except OSError as ex: | ||||
|                 if ex.errno == 13: | ||||
|                     raise Pebkac(500, "the server OS denied write-access") | ||||
|  | ||||
|                 raise Pebkac(500, "mkdir failed:\n" + min_ex()) | ||||
|             except: | ||||
|                 raise Pebkac(500, "mkdir failed, check the logs") | ||||
|                 raise Pebkac(500, min_ex()) | ||||
|  | ||||
|         vpath = "{}/{}".format(self.vpath, sanitized).lstrip("/") | ||||
|         self.redirect(vpath) | ||||
| @@ -753,7 +796,7 @@ class HttpCli(object): | ||||
|         self.parser.drop() | ||||
|  | ||||
|         nullwrite = self.args.nw | ||||
|         vfs, rem = self.conn.auth.vfs.get(self.vpath, self.uname, False, True) | ||||
|         vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True) | ||||
|         self._assert_safe_rem(rem) | ||||
|  | ||||
|         if not new_file.endswith(".md"): | ||||
| @@ -777,7 +820,7 @@ class HttpCli(object): | ||||
|  | ||||
|     def handle_plain_upload(self): | ||||
|         nullwrite = self.args.nw | ||||
|         vfs, rem = self.conn.auth.vfs.get(self.vpath, self.uname, False, True) | ||||
|         vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True) | ||||
|         self._assert_safe_rem(rem) | ||||
|  | ||||
|         files = [] | ||||
| @@ -814,8 +857,14 @@ class HttpCli(object): | ||||
|                             raise Pebkac(400, "empty files in post") | ||||
|  | ||||
|                         files.append([sz, sha512_hex, p_file, fname]) | ||||
|                         dbv, vrem = vfs.get_dbv(rem) | ||||
|                         self.conn.hsrv.broker.put( | ||||
|                             False, "up2k.hash_file", vfs.realpath, vfs.flags, rem, fname | ||||
|                             False, | ||||
|                             "up2k.hash_file", | ||||
|                             dbv.realpath, | ||||
|                             dbv.flags, | ||||
|                             vrem, | ||||
|                             fname, | ||||
|                         ) | ||||
|                         self.conn.nbyte += sz | ||||
|  | ||||
| @@ -845,12 +894,16 @@ class HttpCli(object): | ||||
|         status = "OK" | ||||
|         if errmsg: | ||||
|             self.log(errmsg) | ||||
|             errmsg = "ERROR: " + errmsg | ||||
|             status = "ERROR" | ||||
|  | ||||
|         msg = "{} // {} bytes // {:.3f} MiB/s\n".format(status, sz_total, spd) | ||||
|         jmsg = {"status": status, "sz": sz_total, "mbps": round(spd, 3), "files": []} | ||||
|  | ||||
|         if errmsg: | ||||
|             msg += errmsg + "\n" | ||||
|             jmsg["error"] = errmsg | ||||
|             errmsg = "ERROR: " + errmsg | ||||
|  | ||||
|         for sz, sha512, ofn, lfn in files: | ||||
|             vpath = (self.vpath + "/" if self.vpath else "") + lfn | ||||
|             msg += 'sha512: {} // {} bytes // <a href="/{}">{}</a>\n'.format( | ||||
| @@ -882,11 +935,21 @@ class HttpCli(object): | ||||
|                 ft = "{}\n{}\n{}\n".format(ft, msg.rstrip(), errmsg) | ||||
|                 f.write(ft.encode("utf-8")) | ||||
|  | ||||
|         status = 400 if errmsg else 200 | ||||
|         if "j" in self.uparam: | ||||
|             jtxt = json.dumps(jmsg, indent=2, sort_keys=True) | ||||
|             self.reply(jtxt.encode("utf-8", "replace"), mime="application/json") | ||||
|             jtxt = json.dumps(jmsg, indent=2, sort_keys=True).encode("utf-8", "replace") | ||||
|             self.reply(jtxt, mime="application/json", status=status) | ||||
|         else: | ||||
|             self.redirect(self.vpath, msg=msg, flavor="return to", click=False) | ||||
|             self.redirect( | ||||
|                 self.vpath, | ||||
|                 msg=msg, | ||||
|                 flavor="return to", | ||||
|                 click=False, | ||||
|                 status=status, | ||||
|             ) | ||||
|  | ||||
|         if errmsg: | ||||
|             return False | ||||
|  | ||||
|         self.parser.drop() | ||||
|         return True | ||||
| @@ -898,7 +961,7 @@ class HttpCli(object): | ||||
|             raise Pebkac(400, "could not read lastmod from request") | ||||
|  | ||||
|         nullwrite = self.args.nw | ||||
|         vfs, rem = self.conn.auth.vfs.get(self.vpath, self.uname, False, True) | ||||
|         vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True) | ||||
|         self._assert_safe_rem(rem) | ||||
|  | ||||
|         # TODO: | ||||
| @@ -991,6 +1054,8 @@ class HttpCli(object): | ||||
|         cli_lastmod = self.headers.get("if-modified-since") | ||||
|         if cli_lastmod: | ||||
|             try: | ||||
|                 # some browser append "; length=573" | ||||
|                 cli_lastmod = cli_lastmod.split(";")[0].strip() | ||||
|                 cli_dt = time.strptime(cli_lastmod, HTTP_TS_FMT) | ||||
|                 cli_ts = calendar.timegm(cli_dt) | ||||
|                 return file_lastmod, int(file_ts) > int(cli_ts) | ||||
| @@ -1137,7 +1202,7 @@ class HttpCli(object): | ||||
|         # | ||||
|         # send reply | ||||
|  | ||||
|         if not is_compressed: | ||||
|         if not is_compressed and "cache" not in self.uparam: | ||||
|             self.out_headers.update(NO_CACHE) | ||||
|  | ||||
|         self.out_headers["Accept-Ranges"] = "bytes" | ||||
| @@ -1160,7 +1225,8 @@ class HttpCli(object): | ||||
|             if use_sendfile: | ||||
|                 remains = sendfile_kern(lower, upper, f, self.s) | ||||
|             else: | ||||
|                 remains = sendfile_py(lower, upper, f, self.s) | ||||
|                 actor = self.conn if self.is_mp else None | ||||
|                 remains = sendfile_py(lower, upper, f, self.s, actor) | ||||
|  | ||||
|         if remains > 0: | ||||
|             logmsg += " \033[31m" + unicode(upper - remains) + "\033[0m" | ||||
| @@ -1333,11 +1399,13 @@ class HttpCli(object): | ||||
|             for y in [self.rvol, self.wvol, self.avol] | ||||
|         ] | ||||
|  | ||||
|         vstate = {} | ||||
|         if self.avol and not self.args.no_rescan: | ||||
|             x = self.conn.hsrv.broker.put(True, "up2k.get_volstate") | ||||
|             vstate = json.loads(x.get()) | ||||
|             vstate = {("/" + k).rstrip("/") + "/": v for k, v in vstate.items()} | ||||
|             x = self.conn.hsrv.broker.put(True, "up2k.get_state") | ||||
|             vs = json.loads(x.get()) | ||||
|             vstate = {("/" + k).rstrip("/") + "/": v for k, v in vs["volstate"].items()} | ||||
|         else: | ||||
|             vstate = {} | ||||
|             vs = {"scanning": None, "hashq": None, "tagq": None, "mtpq": None} | ||||
|  | ||||
|         html = self.j2( | ||||
|             "splash", | ||||
| @@ -1346,6 +1414,10 @@ class HttpCli(object): | ||||
|             wvol=wvol, | ||||
|             avol=avol, | ||||
|             vstate=vstate, | ||||
|             scanning=vs["scanning"], | ||||
|             hashq=vs["hashq"], | ||||
|             tagq=vs["tagq"], | ||||
|             mtpq=vs["mtpq"], | ||||
|             url_suf=suf, | ||||
|         ) | ||||
|         self.reply(html.encode("utf-8"), headers=NO_STORE) | ||||
| @@ -1358,9 +1430,10 @@ class HttpCli(object): | ||||
|         if self.args.no_rescan: | ||||
|             raise Pebkac(403, "disabled by argv") | ||||
|  | ||||
|         vn, _ = self.auth.vfs.get(self.vpath, self.uname, True, True) | ||||
|         vn, _ = self.asrv.vfs.get(self.vpath, self.uname, True, True) | ||||
|  | ||||
|         args = [self.asrv.vfs.all_vols, [vn.vpath]] | ||||
|  | ||||
|         args = [self.auth.vfs.all_vols, [vn.vpath]] | ||||
|         x = self.conn.hsrv.broker.put(True, "up2k.rescan", *args) | ||||
|         x = x.get() | ||||
|         if not x: | ||||
| @@ -1376,17 +1449,8 @@ class HttpCli(object): | ||||
|         if self.args.no_stack: | ||||
|             raise Pebkac(403, "disabled by argv") | ||||
|  | ||||
|         ret = [] | ||||
|         names = dict([(t.ident, t.name) for t in threading.enumerate()]) | ||||
|         for tid, stack in sys._current_frames().items(): | ||||
|             ret.append("\n\n# {} ({:x})".format(names.get(tid), tid)) | ||||
|             for fn, lno, name, line in traceback.extract_stack(stack): | ||||
|                 ret.append('File: "{}", line {}, in {}'.format(fn, lno, name)) | ||||
|                 if line: | ||||
|                     ret.append("  " + str(line.strip())) | ||||
|  | ||||
|         ret = ("<pre>" + "\n".join(ret)).encode("utf-8") | ||||
|         self.reply(ret) | ||||
|         ret = "<pre>{}\n{}".format(time.time(), alltrace()) | ||||
|         self.reply(ret.encode("utf-8")) | ||||
|  | ||||
|     def tx_tree(self): | ||||
|         top = self.uparam["tree"] or "" | ||||
| @@ -1416,9 +1480,9 @@ class HttpCli(object): | ||||
|             ret["k" + quotep(excl)] = sub | ||||
|  | ||||
|         try: | ||||
|             vn, rem = self.auth.vfs.get(top, self.uname, True, False) | ||||
|             vn, rem = self.asrv.vfs.get(top, self.uname, True, False) | ||||
|             fsroot, vfs_ls, vfs_virt = vn.ls( | ||||
|                 rem, self.uname, not self.args.no_scandir, True | ||||
|                 rem, self.uname, not self.args.no_scandir, incl_wo=True | ||||
|             ) | ||||
|         except: | ||||
|             vfs_ls = [] | ||||
| @@ -1457,35 +1521,51 @@ class HttpCli(object): | ||||
|  | ||||
|                 vpnodes.append([quotep(vpath) + "/", html_escape(node, crlf=True)]) | ||||
|  | ||||
|         vn, rem = self.auth.vfs.get( | ||||
|         vn, rem = self.asrv.vfs.get( | ||||
|             self.vpath, self.uname, self.readable, self.writable | ||||
|         ) | ||||
|         abspath = vn.canonical(rem) | ||||
|         dbv, vrem = vn.get_dbv(rem) | ||||
|  | ||||
|         try: | ||||
|             st = os.stat(fsenc(abspath)) | ||||
|         except: | ||||
|             raise Pebkac(404) | ||||
|  | ||||
|         if self.readable and not stat.S_ISDIR(st.st_mode): | ||||
|         if self.readable: | ||||
|             if rem.startswith(".hist/up2k."): | ||||
|                 raise Pebkac(403) | ||||
|  | ||||
|             is_dir = stat.S_ISDIR(st.st_mode) | ||||
|             th_fmt = self.uparam.get("th") | ||||
|             if th_fmt is not None: | ||||
|                 if is_dir: | ||||
|                     for fn in ["folder.png", "folder.jpg"]: | ||||
|                         fp = os.path.join(abspath, fn) | ||||
|                         if os.path.exists(fp): | ||||
|                             vrem = "{}/{}".format(vrem.rstrip("/"), fn) | ||||
|                             is_dir = False | ||||
|                             break | ||||
|  | ||||
|                     if is_dir: | ||||
|                         return self.tx_ico("a.folder") | ||||
|  | ||||
|                 thp = None | ||||
|                 if self.thumbcli: | ||||
|                     thp = self.thumbcli.get(vn.realpath, rem, int(st.st_mtime), th_fmt) | ||||
|                     thp = self.thumbcli.get( | ||||
|                         dbv.realpath, vrem, int(st.st_mtime), th_fmt | ||||
|                     ) | ||||
|  | ||||
|                 if thp: | ||||
|                     return self.tx_file(thp) | ||||
|  | ||||
|                 return self.tx_ico(rem) | ||||
|  | ||||
|             if abspath.endswith(".md") and "raw" not in self.uparam: | ||||
|                 return self.tx_md(abspath) | ||||
|             if not is_dir: | ||||
|                 if abspath.endswith(".md") and "raw" not in self.uparam: | ||||
|                     return self.tx_md(abspath) | ||||
|  | ||||
|             return self.tx_file(abspath) | ||||
|                 return self.tx_file(abspath) | ||||
|  | ||||
|         srv_info = [] | ||||
|  | ||||
| @@ -1585,7 +1665,7 @@ class HttpCli(object): | ||||
|                 return self.tx_zip(k, v, vn, rem, [], self.args.ed) | ||||
|  | ||||
|         fsroot, vfs_ls, vfs_virt = vn.ls( | ||||
|             rem, self.uname, not self.args.no_scandir, True | ||||
|             rem, self.uname, not self.args.no_scandir, incl_wo=True | ||||
|         ) | ||||
|         stats = {k: v for k, v in vfs_ls} | ||||
|         vfs_ls = [x[0] for x in vfs_ls] | ||||
| @@ -1618,7 +1698,7 @@ class HttpCli(object): | ||||
|         icur = None | ||||
|         if "e2t" in vn.flags: | ||||
|             idx = self.conn.get_u2idx() | ||||
|             icur = idx.get_cur(vn.realpath) | ||||
|             icur = idx.get_cur(dbv.realpath) | ||||
|  | ||||
|         dirs = [] | ||||
|         files = [] | ||||
| @@ -1686,6 +1766,9 @@ class HttpCli(object): | ||||
|             rd = f["rd"] | ||||
|             del f["rd"] | ||||
|             if icur: | ||||
|                 if vn != dbv: | ||||
|                     _, rd = vn.get_dbv(rd) | ||||
|  | ||||
|                 q = "select w from up where rd = ? and fn = ?" | ||||
|                 try: | ||||
|                     r = icur.execute(q, (rd, fn)).fetchone() | ||||
| @@ -1726,9 +1809,13 @@ class HttpCli(object): | ||||
|         j2a["files"] = dirs + files | ||||
|         j2a["logues"] = logues | ||||
|         j2a["taglist"] = taglist | ||||
|  | ||||
|         if "mte" in vn.flags: | ||||
|             j2a["tag_order"] = json.dumps(vn.flags["mte"].split(",")) | ||||
|  | ||||
|         if self.args.css_browser: | ||||
|             j2a["css"] = self.args.css_browser | ||||
|  | ||||
|         html = self.j2(tpl, **j2a) | ||||
|         self.reply(html.encode("utf-8", "replace"), headers=NO_STORE) | ||||
|         return True | ||||
|   | ||||
| @@ -34,7 +34,8 @@ class HttpConn(object): | ||||
|         self.hsrv = hsrv | ||||
|  | ||||
|         self.args = hsrv.args | ||||
|         self.auth = hsrv.auth | ||||
|         self.asrv = hsrv.asrv | ||||
|         self.is_mp = hsrv.is_mp | ||||
|         self.cert_path = hsrv.cert_path | ||||
|  | ||||
|         enth = HAVE_PIL and not self.args.no_thumb | ||||
| @@ -42,6 +43,7 @@ class HttpConn(object): | ||||
|         self.ico = Ico(self.args) | ||||
|  | ||||
|         self.t0 = time.time() | ||||
|         self.stopping = False | ||||
|         self.nbyte = 0 | ||||
|         self.workload = 0 | ||||
|         self.u2idx = None | ||||
| @@ -49,6 +51,14 @@ class HttpConn(object): | ||||
|         self.lf_url = re.compile(self.args.lf_url) if self.args.lf_url else None | ||||
|         self.set_rproxy() | ||||
|  | ||||
|     def shutdown(self): | ||||
|         self.stopping = True | ||||
|         try: | ||||
|             self.s.shutdown(socket.SHUT_RDWR) | ||||
|             self.s.close() | ||||
|         except: | ||||
|             pass | ||||
|  | ||||
|     def set_rproxy(self, ip=None): | ||||
|         if ip is None: | ||||
|             color = 36 | ||||
| @@ -70,7 +80,7 @@ class HttpConn(object): | ||||
|  | ||||
|     def get_u2idx(self): | ||||
|         if not self.u2idx: | ||||
|             self.u2idx = U2idx(self.args, self.log_func) | ||||
|             self.u2idx = U2idx(self) | ||||
|  | ||||
|         return self.u2idx | ||||
|  | ||||
| @@ -162,7 +172,7 @@ class HttpConn(object): | ||||
|                     self.log("client rejected our certificate (nice)") | ||||
|  | ||||
|                 elif "ALERT_CERTIFICATE_UNKNOWN" in em: | ||||
|                     # chrome-android keeps doing this | ||||
|                     # android-chrome keeps doing this | ||||
|                     pass | ||||
|  | ||||
|                 else: | ||||
| @@ -173,7 +183,12 @@ class HttpConn(object): | ||||
|         if not self.sr: | ||||
|             self.sr = Unrecv(self.s) | ||||
|  | ||||
|         while True: | ||||
|         while not self.stopping: | ||||
|             if self.is_mp: | ||||
|                 self.workload += 50 | ||||
|                 if self.workload >= 2 ** 31: | ||||
|                     self.workload = 100 | ||||
|  | ||||
|             cli = HttpCli(self) | ||||
|             if not cli.run(): | ||||
|                 return | ||||
|   | ||||
| @@ -25,8 +25,8 @@ except ImportError: | ||||
|     sys.exit(1) | ||||
|  | ||||
| from .__init__ import E, MACOS | ||||
| from .httpconn import HttpConn | ||||
| from .authsrv import AuthSrv | ||||
| from .httpconn import HttpConn | ||||
|  | ||||
|  | ||||
| class HttpSrv(object): | ||||
| @@ -35,10 +35,12 @@ class HttpSrv(object): | ||||
|     relying on MpSrv for performance (HttpSrv is just plain threads) | ||||
|     """ | ||||
|  | ||||
|     def __init__(self, broker): | ||||
|     def __init__(self, broker, is_mp=False): | ||||
|         self.broker = broker | ||||
|         self.is_mp = is_mp | ||||
|         self.args = broker.args | ||||
|         self.log = broker.log | ||||
|         self.asrv = broker.asrv | ||||
|  | ||||
|         self.disconnect_func = None | ||||
|         self.mutex = threading.Lock() | ||||
| @@ -46,7 +48,6 @@ class HttpSrv(object): | ||||
|         self.clients = {} | ||||
|         self.workload = 0 | ||||
|         self.workload_thr_alive = False | ||||
|         self.auth = AuthSrv(self.args, self.log) | ||||
|  | ||||
|         env = jinja2.Environment() | ||||
|         env.loader = jinja2.FileSystemLoader(os.path.join(E.mod, "web")) | ||||
| @@ -66,7 +67,11 @@ class HttpSrv(object): | ||||
|         if self.args.log_conn: | ||||
|             self.log("%s %s" % addr, "|%sC-cthr" % ("-" * 5,), c="1;30") | ||||
|  | ||||
|         thr = threading.Thread(target=self.thr_client, args=(sck, addr)) | ||||
|         thr = threading.Thread( | ||||
|             target=self.thr_client, | ||||
|             args=(sck, addr), | ||||
|             name="httpsrv-{}-{}".format(addr[0].split(".", 2)[-1][-6:], addr[1]), | ||||
|         ) | ||||
|         thr.daemon = True | ||||
|         thr.start() | ||||
|  | ||||
| @@ -75,7 +80,14 @@ class HttpSrv(object): | ||||
|             return len(self.clients) | ||||
|  | ||||
|     def shutdown(self): | ||||
|         self.log("ok bye") | ||||
|         clients = list(self.clients.keys()) | ||||
|         for cli in clients: | ||||
|             try: | ||||
|                 cli.shutdown() | ||||
|             except: | ||||
|                 pass | ||||
|  | ||||
|         self.log("httpsrv-n", "ok bye") | ||||
|  | ||||
|     def thr_client(self, sck, addr): | ||||
|         """thread managing one tcp client""" | ||||
| @@ -84,32 +96,46 @@ class HttpSrv(object): | ||||
|         cli = HttpConn(sck, addr, self) | ||||
|         with self.mutex: | ||||
|             self.clients[cli] = 0 | ||||
|             self.workload += 50 | ||||
|  | ||||
|             if not self.workload_thr_alive: | ||||
|                 self.workload_thr_alive = True | ||||
|                 thr = threading.Thread(target=self.thr_workload) | ||||
|                 thr.daemon = True | ||||
|                 thr.start() | ||||
|             if self.is_mp: | ||||
|                 self.workload += 50 | ||||
|                 if not self.workload_thr_alive: | ||||
|                     self.workload_thr_alive = True | ||||
|                     thr = threading.Thread( | ||||
|                         target=self.thr_workload, name="httpsrv-workload" | ||||
|                     ) | ||||
|                     thr.daemon = True | ||||
|                     thr.start() | ||||
|  | ||||
|         fno = sck.fileno() | ||||
|         try: | ||||
|             if self.args.log_conn: | ||||
|                 self.log("%s %s" % addr, "|%sC-crun" % ("-" * 6,), c="1;30") | ||||
|  | ||||
|             cli.run() | ||||
|  | ||||
|         except (OSError, socket.error) as ex: | ||||
|             if ex.errno not in [10038, 10054, 107, 57, 9]: | ||||
|                 self.log( | ||||
|                     "%s %s" % addr, | ||||
|                     "run({}): {}".format(fno, ex), | ||||
|                     c=6, | ||||
|                 ) | ||||
|  | ||||
|         finally: | ||||
|             sck = cli.s | ||||
|             if self.args.log_conn: | ||||
|                 self.log("%s %s" % addr, "|%sC-cdone" % ("-" * 7,), c="1;30") | ||||
|  | ||||
|             try: | ||||
|                 fno = sck.fileno() | ||||
|                 sck.shutdown(socket.SHUT_RDWR) | ||||
|                 sck.close() | ||||
|             except (OSError, socket.error) as ex: | ||||
|                 if not MACOS: | ||||
|                     self.log( | ||||
|                         "%s %s" % addr, | ||||
|                         "shut({}): {}".format(sck.fileno(), ex), | ||||
|                         "shut({}): {}".format(fno, ex), | ||||
|                         c="1;30", | ||||
|                     ) | ||||
|                 if ex.errno not in [10038, 10054, 107, 57, 9]: | ||||
|   | ||||
| @@ -1,3 +1,6 @@ | ||||
| # coding: utf-8 | ||||
| from __future__ import print_function, unicode_literals | ||||
|  | ||||
| import hashlib | ||||
| import colorsys | ||||
|  | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| # coding: utf-8 | ||||
| from __future__ import print_function, unicode_literals | ||||
|  | ||||
| import re | ||||
| import os | ||||
| import sys | ||||
| import json | ||||
| @@ -9,7 +8,7 @@ import shutil | ||||
| import subprocess as sp | ||||
|  | ||||
| from .__init__ import PY2, WINDOWS | ||||
| from .util import fsenc, fsdec, REKOBO_LKEY | ||||
| from .util import fsenc, fsdec, uncyg, REKOBO_LKEY | ||||
|  | ||||
| if not PY2: | ||||
|     unicode = str | ||||
| @@ -17,6 +16,7 @@ if not PY2: | ||||
|  | ||||
| def have_ff(cmd): | ||||
|     if PY2: | ||||
|         print("# checking {}".format(cmd)) | ||||
|         cmd = (cmd + " -version").encode("ascii").split(b" ") | ||||
|         try: | ||||
|             sp.Popen(cmd, stdout=sp.PIPE, stderr=sp.PIPE).communicate() | ||||
| @@ -44,6 +44,9 @@ class MParser(object): | ||||
|         while True: | ||||
|             try: | ||||
|                 bp = os.path.expanduser(args) | ||||
|                 if WINDOWS: | ||||
|                     bp = uncyg(bp) | ||||
|  | ||||
|                 if os.path.exists(bp): | ||||
|                     self.bin = bp | ||||
|                     return | ||||
|   | ||||
| @@ -1,3 +1,6 @@ | ||||
| # coding: utf-8 | ||||
| from __future__ import print_function, unicode_literals | ||||
|  | ||||
| import os | ||||
| import tarfile | ||||
| import threading | ||||
| @@ -42,7 +45,7 @@ class StreamTar(object): | ||||
|         fmt = tarfile.GNU_FORMAT | ||||
|         self.tar = tarfile.open(fileobj=self.qfile, mode="w|", format=fmt) | ||||
|  | ||||
|         w = threading.Thread(target=self._gen) | ||||
|         w = threading.Thread(target=self._gen, name="star-gen") | ||||
|         w.daemon = True | ||||
|         w.start() | ||||
|  | ||||
|   | ||||
| @@ -1,3 +1,6 @@ | ||||
| # coding: utf-8 | ||||
| from __future__ import print_function, unicode_literals | ||||
|  | ||||
| import os | ||||
| import time | ||||
| import tempfile | ||||
|   | ||||
| @@ -37,14 +37,13 @@ class SvcHub(object): | ||||
|  | ||||
|         self.log = self._log_disabled if args.q else self._log_enabled | ||||
|  | ||||
|         # jank goes here | ||||
|         auth = AuthSrv(self.args, self.log, False) | ||||
|         if args.ls: | ||||
|             auth.dbg_ls() | ||||
|  | ||||
|         # initiate all services to manage | ||||
|         self.asrv = AuthSrv(self.args, self.log, False) | ||||
|         if args.ls: | ||||
|             self.asrv.dbg_ls() | ||||
|  | ||||
|         self.tcpsrv = TcpSrv(self) | ||||
|         self.up2k = Up2k(self, auth.vfs.all_vols) | ||||
|         self.up2k = Up2k(self) | ||||
|  | ||||
|         self.thumbsrv = None | ||||
|         if not args.no_thumb: | ||||
| @@ -54,7 +53,7 @@ class SvcHub(object): | ||||
|                     msg = "setting --th-no-webp because either libwebp is not available or your Pillow is too old" | ||||
|                     self.log("thumb", msg, c=3) | ||||
|  | ||||
|                 self.thumbsrv = ThumbSrv(self, auth.vfs.all_vols) | ||||
|                 self.thumbsrv = ThumbSrv(self) | ||||
|             else: | ||||
|                 msg = "need Pillow to create thumbnails; for example:\n{}{} -m pip install --user Pillow\n" | ||||
|                 self.log( | ||||
| @@ -71,7 +70,7 @@ class SvcHub(object): | ||||
|         self.broker = Broker(self) | ||||
|  | ||||
|     def run(self): | ||||
|         thr = threading.Thread(target=self.tcpsrv.run) | ||||
|         thr = threading.Thread(target=self.tcpsrv.run, name="svchub-main") | ||||
|         thr.daemon = True | ||||
|         thr.start() | ||||
|  | ||||
| @@ -95,9 +94,11 @@ class SvcHub(object): | ||||
|                         break | ||||
|  | ||||
|                     if n == 3: | ||||
|                         print("waiting for thumbsrv...") | ||||
|                         print("waiting for thumbsrv (10sec)...") | ||||
|  | ||||
|             print("nailed it") | ||||
|             print("nailed it", end="") | ||||
|         finally: | ||||
|             print("\033[0m") | ||||
|  | ||||
|     def _log_disabled(self, src, msg, c=0): | ||||
|         pass | ||||
|   | ||||
| @@ -1,3 +1,6 @@ | ||||
| # coding: utf-8 | ||||
| from __future__ import print_function, unicode_literals | ||||
|  | ||||
| import os | ||||
| import time | ||||
| import zlib | ||||
|   | ||||
| @@ -21,6 +21,7 @@ class TcpSrv(object): | ||||
|         self.log = hub.log | ||||
|  | ||||
|         self.num_clients = Counter() | ||||
|         self.stopping = False | ||||
|  | ||||
|         ip = "127.0.0.1" | ||||
|         eps = {ip: "local only"} | ||||
| @@ -67,7 +68,7 @@ class TcpSrv(object): | ||||
|             ip, port = srv.getsockname() | ||||
|             self.log("tcpsrv", "listening @ {0}:{1}".format(ip, port)) | ||||
|  | ||||
|         while True: | ||||
|         while not self.stopping: | ||||
|             if self.args.log_conn: | ||||
|                 self.log("tcpsrv", "|%sC-ncli" % ("-" * 1,), c="1;30") | ||||
|  | ||||
| @@ -78,8 +79,18 @@ class TcpSrv(object): | ||||
|             if self.args.log_conn: | ||||
|                 self.log("tcpsrv", "|%sC-acc1" % ("-" * 2,), c="1;30") | ||||
|  | ||||
|             ready, _, _ = select.select(self.srv, [], []) | ||||
|             try: | ||||
|                 # macos throws bad-fd | ||||
|                 ready, _, _ = select.select(self.srv, [], []) | ||||
|             except: | ||||
|                 ready = [] | ||||
|                 if not self.stopping: | ||||
|                     raise | ||||
|  | ||||
|             for srv in ready: | ||||
|                 if self.stopping: | ||||
|                     break | ||||
|  | ||||
|                 sck, addr = srv.accept() | ||||
|                 sip, sport = srv.getsockname() | ||||
|                 if self.args.log_conn: | ||||
| @@ -95,6 +106,13 @@ class TcpSrv(object): | ||||
|                 self.hub.broker.put(False, "httpconn", sck, addr) | ||||
|  | ||||
|     def shutdown(self): | ||||
|         self.stopping = True | ||||
|         try: | ||||
|             for srv in self.srv: | ||||
|                 srv.close() | ||||
|         except: | ||||
|             pass | ||||
|  | ||||
|         self.log("tcpsrv", "ok bye") | ||||
|  | ||||
|     def detect_interfaces(self, listen_ips): | ||||
|   | ||||
| @@ -1,5 +1,7 @@ | ||||
| # coding: utf-8 | ||||
| from __future__ import print_function, unicode_literals | ||||
|  | ||||
| import os | ||||
| import time | ||||
|  | ||||
| from .util import Cooldown | ||||
| from .th_srv import thumb_path, THUMBABLE, FMT_FF | ||||
| @@ -9,6 +11,7 @@ class ThumbCli(object): | ||||
|     def __init__(self, broker): | ||||
|         self.broker = broker | ||||
|         self.args = broker.args | ||||
|         self.asrv = broker.asrv | ||||
|  | ||||
|         # cache on both sides for less broker spam | ||||
|         self.cooldown = Cooldown(self.args.th_poke) | ||||
| @@ -18,16 +21,19 @@ class ThumbCli(object): | ||||
|         if ext not in THUMBABLE: | ||||
|             return None | ||||
|  | ||||
|         if self.args.no_vthumb and ext in FMT_FF: | ||||
|         is_vid = ext in FMT_FF | ||||
|         if is_vid and self.args.no_vthumb: | ||||
|             return None | ||||
|  | ||||
|         if fmt == "j" and self.args.th_no_jpg: | ||||
|             fmt = "w" | ||||
|  | ||||
|         if fmt == "w" and self.args.th_no_webp: | ||||
|             fmt = "j" | ||||
|         if fmt == "w": | ||||
|             if self.args.th_no_webp or (is_vid and self.args.th_ff_jpg): | ||||
|                 fmt = "j" | ||||
|  | ||||
|         tpath = thumb_path(ptop, rem, mtime, fmt) | ||||
|         histpath = self.asrv.vfs.histtab[ptop] | ||||
|         tpath = thumb_path(histpath, rem, mtime, fmt) | ||||
|         ret = None | ||||
|         try: | ||||
|             st = os.stat(tpath) | ||||
|   | ||||
| @@ -1,5 +1,7 @@ | ||||
| # coding: utf-8 | ||||
| from __future__ import print_function, unicode_literals | ||||
|  | ||||
| import os | ||||
| import sys | ||||
| import time | ||||
| import shutil | ||||
| import base64 | ||||
| @@ -8,7 +10,7 @@ import threading | ||||
| import subprocess as sp | ||||
|  | ||||
| from .__init__ import PY2 | ||||
| from .util import fsenc, mchkcmd, Queue, Cooldown, BytesIO | ||||
| from .util import fsenc, runcmd, Queue, Cooldown, BytesIO, min_ex | ||||
| from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, ffprobe | ||||
|  | ||||
|  | ||||
| @@ -51,7 +53,7 @@ except: | ||||
| # https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html | ||||
| # ffmpeg -formats | ||||
| FMT_PIL = "bmp dib gif icns ico jpg jpeg jp2 jpx pcx png pbm pgm ppm pnm sgi tga tif tiff webp xbm dds xpm" | ||||
| FMT_FF = "av1 asf avi flv m4v mkv mjpeg mjpg mpg mpeg mpg2 mpeg2 mov 3gp mp4 ts mpegts nut ogv ogm rm vob webm wmv" | ||||
| FMT_FF = "av1 asf avi flv m4v mkv mjpeg mjpg mpg mpeg mpg2 mpeg2 h264 avc h265 hevc mov 3gp mp4 ts mpegts nut ogv ogm rm vob webm wmv" | ||||
|  | ||||
| if HAVE_HEIF: | ||||
|     FMT_PIL += " heif heifs heic heics" | ||||
| @@ -71,7 +73,7 @@ if HAVE_FFMPEG and HAVE_FFPROBE: | ||||
|     THUMBABLE.update(FMT_FF) | ||||
|  | ||||
|  | ||||
| def thumb_path(ptop, rem, mtime, fmt): | ||||
| def thumb_path(histpath, rem, mtime, fmt): | ||||
|     # base16 = 16 = 256 | ||||
|     # b64-lc = 38 = 1444 | ||||
|     # base64 = 64 = 4096 | ||||
| @@ -82,26 +84,25 @@ def thumb_path(ptop, rem, mtime, fmt): | ||||
|         fn = rem | ||||
|  | ||||
|     if rd: | ||||
|         h = hashlib.sha512(fsenc(rd)).digest()[:24] | ||||
|         h = hashlib.sha512(fsenc(rd)).digest() | ||||
|         b64 = base64.urlsafe_b64encode(h).decode("ascii")[:24] | ||||
|         rd = "{}/{}/".format(b64[:2], b64[2:4]).lower() + b64 | ||||
|     else: | ||||
|         rd = "top" | ||||
|  | ||||
|     # could keep original filenames but this is safer re pathlen | ||||
|     h = hashlib.sha512(fsenc(fn)).digest()[:24] | ||||
|     h = hashlib.sha512(fsenc(fn)).digest() | ||||
|     fn = base64.urlsafe_b64encode(h).decode("ascii")[:24] | ||||
|  | ||||
|     return "{}/.hist/th/{}/{}.{:x}.{}".format( | ||||
|         ptop, rd, fn, int(mtime), "webp" if fmt == "w" else "jpg" | ||||
|     return "{}/th/{}/{}.{:x}.{}".format( | ||||
|         histpath, rd, fn, int(mtime), "webp" if fmt == "w" else "jpg" | ||||
|     ) | ||||
|  | ||||
|  | ||||
| class ThumbSrv(object): | ||||
|     def __init__(self, hub, vols): | ||||
|     def __init__(self, hub): | ||||
|         self.hub = hub | ||||
|         self.vols = [v.realpath for v in vols.values()] | ||||
|  | ||||
|         self.asrv = hub.asrv | ||||
|         self.args = hub.args | ||||
|         self.log_func = hub.log | ||||
|  | ||||
| @@ -114,8 +115,10 @@ class ThumbSrv(object): | ||||
|         self.stopping = False | ||||
|         self.nthr = os.cpu_count() if hasattr(os, "cpu_count") else 4 | ||||
|         self.q = Queue(self.nthr * 4) | ||||
|         for _ in range(self.nthr): | ||||
|             t = threading.Thread(target=self.worker) | ||||
|         for n in range(self.nthr): | ||||
|             t = threading.Thread( | ||||
|                 target=self.worker, name="thumb-{}-{}".format(n, self.nthr) | ||||
|             ) | ||||
|             t.daemon = True | ||||
|             t.start() | ||||
|  | ||||
| @@ -131,7 +134,7 @@ class ThumbSrv(object): | ||||
|             msg += ", ".join(missing) | ||||
|             self.log(msg, c=3) | ||||
|  | ||||
|         t = threading.Thread(target=self.cleaner) | ||||
|         t = threading.Thread(target=self.cleaner, name="thumb-cleaner") | ||||
|         t.daemon = True | ||||
|         t.start() | ||||
|  | ||||
| @@ -148,9 +151,11 @@ class ThumbSrv(object): | ||||
|             return not self.nthr | ||||
|  | ||||
|     def get(self, ptop, rem, mtime, fmt): | ||||
|         tpath = thumb_path(ptop, rem, mtime, fmt) | ||||
|         histpath = self.asrv.vfs.histtab[ptop] | ||||
|         tpath = thumb_path(histpath, rem, mtime, fmt) | ||||
|         abspath = os.path.join(ptop, rem) | ||||
|         cond = threading.Condition() | ||||
|         cond = threading.Condition(self.mutex) | ||||
|         do_conv = False | ||||
|         with self.mutex: | ||||
|             try: | ||||
|                 self.busy[tpath].append(cond) | ||||
| @@ -168,8 +173,11 @@ class ThumbSrv(object): | ||||
|                         f.write(fsenc(os.path.dirname(abspath))) | ||||
|  | ||||
|                 self.busy[tpath] = [cond] | ||||
|                 self.q.put([abspath, tpath]) | ||||
|                 self.log("conv {} \033[0m{}".format(tpath, abspath), c=6) | ||||
|                 do_conv = True | ||||
|  | ||||
|         if do_conv: | ||||
|             self.q.put([abspath, tpath]) | ||||
|             self.log("conv {} \033[0m{}".format(tpath, abspath), c=6) | ||||
|  | ||||
|         while not self.stopping: | ||||
|             with self.mutex: | ||||
| @@ -177,7 +185,7 @@ class ThumbSrv(object): | ||||
|                     break | ||||
|  | ||||
|             with cond: | ||||
|                 cond.wait() | ||||
|                 cond.wait(3) | ||||
|  | ||||
|         try: | ||||
|             st = os.stat(tpath) | ||||
| @@ -206,9 +214,9 @@ class ThumbSrv(object): | ||||
|             if fun: | ||||
|                 try: | ||||
|                     fun(abspath, tpath) | ||||
|                 except Exception as ex: | ||||
|                     msg = "{} failed on {}\n  {!r}" | ||||
|                     self.log(msg.format(fun.__name__, abspath, ex), 3) | ||||
|                 except: | ||||
|                     msg = "{} failed on {}\n{}" | ||||
|                     self.log(msg.format(fun.__name__, abspath, min_ex()), 3) | ||||
|                     with open(tpath, "wb") as _: | ||||
|                         pass | ||||
|  | ||||
| @@ -240,8 +248,8 @@ class ThumbSrv(object): | ||||
|             except: | ||||
|                 im.thumbnail(self.res) | ||||
|  | ||||
|             if im.mode not in ("RGB", "L"): | ||||
|                 im = im.convert("RGB") | ||||
|             fmts = ["RGB", "L"] | ||||
|             args = {"quality": 40} | ||||
|  | ||||
|             if tpath.endswith(".webp"): | ||||
|                 # quality 80 = pillow-default | ||||
| @@ -249,15 +257,27 @@ class ThumbSrv(object): | ||||
|                 # method 0 = pillow-default, fast | ||||
|                 # method 4 = ffmpeg-default | ||||
|                 # method 6 = max, slow | ||||
|                 im.save(tpath, quality=40, method=6) | ||||
|                 fmts += ["RGBA", "LA"] | ||||
|                 args["method"] = 6 | ||||
|             else: | ||||
|                 im.save(tpath, quality=40)  # default=75 | ||||
|                 pass  # default q = 75 | ||||
|  | ||||
|             if im.mode not in fmts: | ||||
|                 print("conv {}".format(im.mode)) | ||||
|                 im = im.convert("RGB") | ||||
|  | ||||
|             im.save(tpath, quality=40, method=6) | ||||
|  | ||||
|     def conv_ffmpeg(self, abspath, tpath): | ||||
|         ret, _ = ffprobe(abspath) | ||||
|  | ||||
|         dur = ret[".dur"][1] if ".dur" in ret else 4 | ||||
|         seek = "{:.0f}".format(dur / 3) | ||||
|         ext = abspath.rsplit(".")[-1] | ||||
|         if ext in ["h264", "h265"]: | ||||
|             seek = [] | ||||
|         else: | ||||
|             dur = ret[".dur"][1] if ".dur" in ret else 4 | ||||
|             seek = "{:.0f}".format(dur / 3) | ||||
|             seek = [b"-ss", seek.encode("utf-8")] | ||||
|  | ||||
|         scale = "scale={0}:{1}:force_original_aspect_ratio=" | ||||
|         if self.args.th_no_crop: | ||||
| @@ -266,19 +286,20 @@ class ThumbSrv(object): | ||||
|             scale += "increase,crop={0}:{1},setsar=1:1" | ||||
|  | ||||
|         scale = scale.format(*list(self.res)).encode("utf-8") | ||||
|         # fmt: off | ||||
|         cmd = [ | ||||
|             b"ffmpeg", | ||||
|             b"-nostdin", | ||||
|             b"-hide_banner", | ||||
|             b"-ss", | ||||
|             seek, | ||||
|             b"-i", | ||||
|             fsenc(abspath), | ||||
|             b"-vf", | ||||
|             scale, | ||||
|             b"-vframes", | ||||
|             b"1", | ||||
|             b"-v", b"error", | ||||
|             b"-hide_banner" | ||||
|         ] | ||||
|         cmd += seek | ||||
|         cmd += [ | ||||
|             b"-i", fsenc(abspath), | ||||
|             b"-vf", scale, | ||||
|             b"-vframes", b"1", | ||||
|         ] | ||||
|         # fmt: on | ||||
|  | ||||
|         if tpath.endswith(".jpg"): | ||||
|             cmd += [ | ||||
| @@ -295,7 +316,11 @@ class ThumbSrv(object): | ||||
|  | ||||
|         cmd += [fsenc(tpath)] | ||||
|  | ||||
|         mchkcmd(cmd) | ||||
|         ret, sout, serr = runcmd(*cmd) | ||||
|         if ret != 0: | ||||
|             msg = ["ff: {}".format(x) for x in serr.split("\n")] | ||||
|             self.log("FFmpeg failed:\n" + "\n".join(msg), c="1;30") | ||||
|             raise sp.CalledProcessError(ret, (cmd[0], b"...", cmd[-1])) | ||||
|  | ||||
|     def poke(self, tdir): | ||||
|         if not self.poke_cd.poke(tdir): | ||||
| @@ -314,26 +339,32 @@ class ThumbSrv(object): | ||||
|         interval = self.args.th_clean | ||||
|         while True: | ||||
|             time.sleep(interval) | ||||
|             for vol in self.vols: | ||||
|                 vol += "/.hist/th" | ||||
|                 self.log("\033[Jcln {}/\033[A".format(vol)) | ||||
|                 self.clean(vol) | ||||
|             ndirs = 0 | ||||
|             for vol, histpath in self.asrv.vfs.histtab.items(): | ||||
|                 if histpath.startswith(vol): | ||||
|                     self.log("\033[Jcln {}/\033[A".format(histpath)) | ||||
|                 else: | ||||
|                     self.log("\033[Jcln {} ({})/\033[A".format(histpath, vol)) | ||||
|  | ||||
|             self.log("\033[Jcln ok") | ||||
|                 ndirs += self.clean(histpath) | ||||
|  | ||||
|     def clean(self, vol): | ||||
|         # self.log("cln {}".format(vol)) | ||||
|             self.log("\033[Jcln ok; rm {} dirs".format(ndirs)) | ||||
|  | ||||
|     def clean(self, histpath): | ||||
|         thumbpath = os.path.join(histpath, "th") | ||||
|         # self.log("cln {}".format(thumbpath)) | ||||
|         maxage = self.args.th_maxage | ||||
|         now = time.time() | ||||
|         prev_b64 = None | ||||
|         prev_fp = None | ||||
|         try: | ||||
|             ents = os.listdir(vol) | ||||
|             ents = os.listdir(thumbpath) | ||||
|         except: | ||||
|             return | ||||
|             return 0 | ||||
|  | ||||
|         ndirs = 0 | ||||
|         for f in sorted(ents): | ||||
|             fp = os.path.join(vol, f) | ||||
|             fp = os.path.join(thumbpath, f) | ||||
|             cmp = fp.lower().replace("\\", "/") | ||||
|  | ||||
|             # "top" or b64 prefix/full (a folder) | ||||
| @@ -348,10 +379,11 @@ class ThumbSrv(object): | ||||
|                                 break | ||||
|  | ||||
|                         if safe: | ||||
|                             ndirs += 1 | ||||
|                             self.log("rm -rf [{}]".format(fp)) | ||||
|                             shutil.rmtree(fp, ignore_errors=True) | ||||
|                 else: | ||||
|                     self.clean(fp) | ||||
|                     ndirs += self.clean(fp) | ||||
|                 continue | ||||
|  | ||||
|             # thumb file | ||||
| @@ -373,3 +405,5 @@ class ThumbSrv(object): | ||||
|  | ||||
|             prev_b64 = b64 | ||||
|             prev_fp = fp | ||||
|  | ||||
|         return ndirs | ||||
|   | ||||
| @@ -7,7 +7,7 @@ import time | ||||
| import threading | ||||
| from datetime import datetime | ||||
|  | ||||
| from .util import u8safe, s3dec, html_escape, Pebkac | ||||
| from .util import s3dec, Pebkac, min_ex | ||||
| from .up2k import up2k_wark_from_hashlist | ||||
|  | ||||
|  | ||||
| @@ -19,10 +19,11 @@ except: | ||||
|  | ||||
|  | ||||
| class U2idx(object): | ||||
|     def __init__(self, args, log_func): | ||||
|         self.args = args | ||||
|         self.log_func = log_func | ||||
|         self.timeout = args.srch_time | ||||
|     def __init__(self, conn): | ||||
|         self.log_func = conn.log_func | ||||
|         self.asrv = conn.asrv | ||||
|         self.args = conn.args | ||||
|         self.timeout = self.args.srch_time | ||||
|  | ||||
|         if not HAVE_SQLITE3: | ||||
|             self.log("could not load sqlite3; searchign wqill be disabled") | ||||
| @@ -52,18 +53,20 @@ class U2idx(object): | ||||
|  | ||||
|         try: | ||||
|             return self.run_query(vols, uq, uv)[0] | ||||
|         except Exception as ex: | ||||
|             raise Pebkac(500, repr(ex)) | ||||
|         except: | ||||
|             raise Pebkac(500, min_ex()) | ||||
|  | ||||
|     def get_cur(self, ptop): | ||||
|         cur = self.cur.get(ptop) | ||||
|         if cur: | ||||
|             return cur | ||||
|  | ||||
|         cur = _open(ptop) | ||||
|         if not cur: | ||||
|         histpath = self.asrv.vfs.histtab[ptop] | ||||
|         db_path = os.path.join(histpath, "up2k.db") | ||||
|         if not os.path.exists(db_path): | ||||
|             return None | ||||
|  | ||||
|         cur = sqlite3.connect(db_path).cursor() | ||||
|         self.cur[ptop] = cur | ||||
|         return cur | ||||
|  | ||||
| @@ -192,6 +195,7 @@ class U2idx(object): | ||||
|                 self.active_id, | ||||
|                 done_flag, | ||||
|             ), | ||||
|             name="u2idx-terminator", | ||||
|         ) | ||||
|         thr.daemon = True | ||||
|         thr.start() | ||||
| @@ -241,6 +245,7 @@ class U2idx(object): | ||||
|                 hit["tags"] = tags | ||||
|  | ||||
|             ret.extend(sret) | ||||
|             # print("[{}] {}".format(ptop, sret)) | ||||
|  | ||||
|         done_flag.append(True) | ||||
|         self.active_id = None | ||||
| @@ -261,9 +266,3 @@ class U2idx(object): | ||||
|  | ||||
|         if identifier == self.active_id: | ||||
|             self.active_cur.connection.interrupt() | ||||
|  | ||||
|  | ||||
| def _open(ptop): | ||||
|     db_path = os.path.join(ptop, ".hist", "up2k.db") | ||||
|     if os.path.exists(db_path): | ||||
|         return sqlite3.connect(db_path).cursor() | ||||
|   | ||||
| @@ -16,7 +16,7 @@ import traceback | ||||
| import subprocess as sp | ||||
| from copy import deepcopy | ||||
|  | ||||
| from .__init__ import WINDOWS, ANYWIN | ||||
| from .__init__ import WINDOWS, ANYWIN, PY2 | ||||
| from .util import ( | ||||
|     Pebkac, | ||||
|     Queue, | ||||
| @@ -30,6 +30,7 @@ from .util import ( | ||||
|     s3dec, | ||||
|     statdir, | ||||
|     s2hms, | ||||
|     min_ex, | ||||
| ) | ||||
| from .mtag import MTag, MParser | ||||
|  | ||||
| @@ -39,6 +40,8 @@ try: | ||||
| except: | ||||
|     HAVE_SQLITE3 = False | ||||
|  | ||||
| DB_VER = 4 | ||||
|  | ||||
|  | ||||
| class Up2k(object): | ||||
|     """ | ||||
| @@ -48,8 +51,9 @@ class Up2k(object): | ||||
|         * ~/.config flatfiles for active jobs | ||||
|     """ | ||||
|  | ||||
|     def __init__(self, hub, all_vols): | ||||
|     def __init__(self, hub): | ||||
|         self.hub = hub | ||||
|         self.asrv = hub.asrv | ||||
|         self.args = hub.args | ||||
|         self.log_func = hub.log | ||||
|  | ||||
| @@ -60,6 +64,8 @@ class Up2k(object): | ||||
|         self.mutex = threading.Lock() | ||||
|         self.hashq = Queue() | ||||
|         self.tagq = Queue() | ||||
|         self.n_hashq = 0 | ||||
|         self.n_tagq = 0 | ||||
|         self.volstate = {} | ||||
|         self.registry = {} | ||||
|         self.entags = {} | ||||
| @@ -83,56 +89,84 @@ class Up2k(object): | ||||
|         if ANYWIN: | ||||
|             # usually fails to set lastmod too quickly | ||||
|             self.lastmod_q = Queue() | ||||
|             thr = threading.Thread(target=self._lastmodder) | ||||
|             thr = threading.Thread(target=self._lastmodder, name="up2k-lastmod") | ||||
|             thr.daemon = True | ||||
|             thr.start() | ||||
|  | ||||
|         # static | ||||
|         self.r_hash = re.compile("^[0-9a-zA-Z_-]{43}$") | ||||
|         self.r_hash = re.compile("^[0-9a-zA-Z_-]{44}$") | ||||
|  | ||||
|         if not HAVE_SQLITE3: | ||||
|             self.log("could not initialize sqlite3, will use in-memory registry only") | ||||
|  | ||||
|         if self.args.no_fastboot: | ||||
|             self.deferred_init(all_vols) | ||||
|             self.deferred_init() | ||||
|         else: | ||||
|             t = threading.Thread(target=self.deferred_init, args=(all_vols,)) | ||||
|             t = threading.Thread( | ||||
|                 target=self.deferred_init, | ||||
|                 name="up2k-deferred-init", | ||||
|             ) | ||||
|             t.daemon = True | ||||
|             t.start() | ||||
|  | ||||
|     def deferred_init(self, all_vols): | ||||
|     def deferred_init(self): | ||||
|         all_vols = self.asrv.vfs.all_vols | ||||
|         have_e2d = self.init_indexes(all_vols) | ||||
|  | ||||
|         if have_e2d: | ||||
|             thr = threading.Thread(target=self._snapshot) | ||||
|             thr = threading.Thread(target=self._snapshot, name="up2k-snapshot") | ||||
|             thr.daemon = True | ||||
|             thr.start() | ||||
|  | ||||
|             thr = threading.Thread(target=self._hasher) | ||||
|             thr = threading.Thread(target=self._hasher, name="up2k-hasher") | ||||
|             thr.daemon = True | ||||
|             thr.start() | ||||
|  | ||||
|             if self.mtag: | ||||
|                 thr = threading.Thread(target=self._tagger) | ||||
|                 thr = threading.Thread(target=self._tagger, name="up2k-tagger") | ||||
|                 thr.daemon = True | ||||
|                 thr.start() | ||||
|  | ||||
|                 thr = threading.Thread(target=self._run_all_mtp) | ||||
|                 thr = threading.Thread(target=self._run_all_mtp, name="up2k-mtp-init") | ||||
|                 thr.daemon = True | ||||
|                 thr.start() | ||||
|  | ||||
|     def log(self, msg, c=0): | ||||
|         self.log_func("up2k", msg + "\033[K", c) | ||||
|  | ||||
|     def get_volstate(self): | ||||
|         return json.dumps(self.volstate, indent=4) | ||||
|     def get_state(self): | ||||
|         mtpq = 0 | ||||
|         q = "select count(w) from mt where k = 't:mtp'" | ||||
|         got_lock = False if PY2 else self.mutex.acquire(timeout=0.5) | ||||
|         if got_lock: | ||||
|             for cur in self.cur.values(): | ||||
|                 try: | ||||
|                     mtpq += cur.execute(q).fetchone()[0] | ||||
|                 except: | ||||
|                     pass | ||||
|             self.mutex.release() | ||||
|         else: | ||||
|             mtpq = "?" | ||||
|  | ||||
|         ret = { | ||||
|             "volstate": self.volstate, | ||||
|             "scanning": hasattr(self, "pp"), | ||||
|             "hashq": self.n_hashq, | ||||
|             "tagq": self.n_tagq, | ||||
|             "mtpq": mtpq, | ||||
|         } | ||||
|         return json.dumps(ret, indent=4) | ||||
|  | ||||
|     def rescan(self, all_vols, scan_vols): | ||||
|         if hasattr(self, "pp"): | ||||
|             return "cannot initiate; scan is already in progress" | ||||
|  | ||||
|         args = (all_vols, scan_vols) | ||||
|         t = threading.Thread(target=self.init_indexes, args=args) | ||||
|         t = threading.Thread( | ||||
|             target=self.init_indexes, | ||||
|             args=args, | ||||
|             name="up2k-rescan-{}".format(scan_vols[0]), | ||||
|         ) | ||||
|         t.daemon = True | ||||
|         t.start() | ||||
|         return None | ||||
| @@ -178,23 +212,27 @@ class Up2k(object): | ||||
|                 self.log(msg, c=3) | ||||
|  | ||||
|         live_vols = [] | ||||
|         for vol in vols: | ||||
|             try: | ||||
|                 os.listdir(vol.realpath) | ||||
|             except: | ||||
|                 self.volstate[vol.vpath] = "OFFLINE (cannot access folder)" | ||||
|                 self.log("cannot access " + vol.realpath, c=1) | ||||
|                 continue | ||||
|         with self.mutex: | ||||
|             # only need to protect register_vpath but all in one go feels right | ||||
|             for vol in vols: | ||||
|                 try: | ||||
|                     os.listdir(vol.realpath) | ||||
|                 except: | ||||
|                     self.volstate[vol.vpath] = "OFFLINE (cannot access folder)" | ||||
|                     self.log("cannot access " + vol.realpath, c=1) | ||||
|                     continue | ||||
|  | ||||
|             if not self.register_vpath(vol.realpath, vol.flags): | ||||
|                 # self.log("db not enabled for {}".format(m, vol.realpath)) | ||||
|                 continue | ||||
|                 if scan_vols and vol.vpath not in scan_vols: | ||||
|                     continue | ||||
|  | ||||
|                 if not self.register_vpath(vol.realpath, vol.flags): | ||||
|                     # self.log("db not enable for {}".format(m, vol.realpath)) | ||||
|                     continue | ||||
|  | ||||
|             if vol.vpath in scan_vols or not scan_vols: | ||||
|                 live_vols.append(vol) | ||||
|  | ||||
|             if vol.vpath not in self.volstate: | ||||
|                 self.volstate[vol.vpath] = "OFFLINE (pending initialization)" | ||||
|                 if vol.vpath not in self.volstate: | ||||
|                     self.volstate[vol.vpath] = "OFFLINE (pending initialization)" | ||||
|  | ||||
|         vols = live_vols | ||||
|         need_vac = {} | ||||
| @@ -271,7 +309,7 @@ class Up2k(object): | ||||
|         if self.mtag: | ||||
|             m = "online (running mtp)" | ||||
|             if scan_vols: | ||||
|                 thr = threading.Thread(target=self._run_all_mtp) | ||||
|                 thr = threading.Thread(target=self._run_all_mtp, name="up2k-mtp-scan") | ||||
|                 thr.daemon = True | ||||
|         else: | ||||
|             del self.pp | ||||
| @@ -286,9 +324,13 @@ class Up2k(object): | ||||
|         return have_e2d | ||||
|  | ||||
|     def register_vpath(self, ptop, flags): | ||||
|         db_path = os.path.join(ptop, ".hist", "up2k.db") | ||||
|         histpath = self.asrv.vfs.histtab[ptop] | ||||
|         db_path = os.path.join(histpath, "up2k.db") | ||||
|         if ptop in self.registry: | ||||
|             return [self.cur[ptop], db_path] | ||||
|             try: | ||||
|                 return [self.cur[ptop], db_path] | ||||
|             except: | ||||
|                 return None | ||||
|  | ||||
|         _, flags = self._expr_idx_filter(flags) | ||||
|  | ||||
| @@ -303,7 +345,7 @@ class Up2k(object): | ||||
|             self.log(" ".join(sorted(a)) + "\033[0m") | ||||
|  | ||||
|         reg = {} | ||||
|         path = os.path.join(ptop, ".hist", "up2k.snap") | ||||
|         path = os.path.join(histpath, "up2k.snap") | ||||
|         if "e2d" in flags and os.path.exists(path): | ||||
|             with gzip.GzipFile(path, "rb") as f: | ||||
|                 j = f.read().decode("utf-8") | ||||
| @@ -327,7 +369,7 @@ class Up2k(object): | ||||
|             return None | ||||
|  | ||||
|         try: | ||||
|             os.mkdir(os.path.join(ptop, ".hist")) | ||||
|             os.makedirs(histpath) | ||||
|         except: | ||||
|             pass | ||||
|  | ||||
| @@ -344,6 +386,7 @@ class Up2k(object): | ||||
|     def _build_file_index(self, vol, all_vols): | ||||
|         do_vac = False | ||||
|         top = vol.realpath | ||||
|         nohash = "dhash" in vol.flags | ||||
|         with self.mutex: | ||||
|             cur, _ = self.register_vpath(top, vol.flags) | ||||
|  | ||||
| @@ -358,7 +401,7 @@ class Up2k(object): | ||||
|             if WINDOWS: | ||||
|                 excl = [x.replace("/", "\\") for x in excl] | ||||
|  | ||||
|             n_add = self._build_dir(dbw, top, set(excl), top) | ||||
|             n_add = self._build_dir(dbw, top, set(excl), top, nohash) | ||||
|             n_rm = self._drop_lost(dbw[0], top) | ||||
|             if dbw[1]: | ||||
|                 self.log("commit {} new files".format(dbw[1])) | ||||
| @@ -366,23 +409,28 @@ class Up2k(object): | ||||
|  | ||||
|             return True, n_add or n_rm or do_vac | ||||
|  | ||||
|     def _build_dir(self, dbw, top, excl, cdir): | ||||
|     def _build_dir(self, dbw, top, excl, cdir, nohash): | ||||
|         self.pp.msg = "a{} {}".format(self.pp.n, cdir) | ||||
|         histdir = os.path.join(top, ".hist") | ||||
|         histpath = self.asrv.vfs.histtab[top] | ||||
|         ret = 0 | ||||
|         for iname, inf in statdir(self.log, not self.args.no_scandir, False, cdir): | ||||
|         g = statdir(self.log, not self.args.no_scandir, False, cdir) | ||||
|         for iname, inf in sorted(g): | ||||
|             abspath = os.path.join(cdir, iname) | ||||
|             lmod = int(inf.st_mtime) | ||||
|             sz = inf.st_size | ||||
|             if stat.S_ISDIR(inf.st_mode): | ||||
|                 if abspath in excl or abspath == histdir: | ||||
|                 if abspath in excl or abspath == histpath: | ||||
|                     continue | ||||
|                 # self.log(" dir: {}".format(abspath)) | ||||
|                 ret += self._build_dir(dbw, top, excl, abspath) | ||||
|                 ret += self._build_dir(dbw, top, excl, abspath, nohash) | ||||
|             else: | ||||
|                 # self.log("file: {}".format(abspath)) | ||||
|                 rp = abspath[len(top) :].replace("\\", "/").strip("/") | ||||
|                 rp = abspath[len(top) + 1 :] | ||||
|                 if WINDOWS: | ||||
|                     rp = rp.replace("\\", "/").strip("/") | ||||
|  | ||||
|                 rd, fn = rp.rsplit("/", 1) if "/" in rp else ["", rp] | ||||
|                 sql = "select * from up where rd = ? and fn = ?" | ||||
|                 sql = "select w, mt, sz from up where rd = ? and fn = ?" | ||||
|                 try: | ||||
|                     c = dbw[0].execute(sql, (rd, fn)) | ||||
|                 except: | ||||
| @@ -391,18 +439,18 @@ class Up2k(object): | ||||
|                 in_db = list(c.fetchall()) | ||||
|                 if in_db: | ||||
|                     self.pp.n -= 1 | ||||
|                     _, dts, dsz, _, _ = in_db[0] | ||||
|                     dw, dts, dsz = in_db[0] | ||||
|                     if len(in_db) > 1: | ||||
|                         m = "WARN: multiple entries: [{}] => [{}] |{}|\n{}" | ||||
|                         rep_db = "\n".join([repr(x) for x in in_db]) | ||||
|                         self.log(m.format(top, rp, len(in_db), rep_db)) | ||||
|                         dts = -1 | ||||
|  | ||||
|                     if dts == lmod and dsz == inf.st_size: | ||||
|                     if dts == lmod and dsz == sz and (nohash or dw[0] != "#"): | ||||
|                         continue | ||||
|  | ||||
|                     m = "reindex [{}] => [{}] ({}/{}) ({}/{})".format( | ||||
|                         top, rp, dts, lmod, dsz, inf.st_size | ||||
|                         top, rp, dts, lmod, dsz, sz | ||||
|                     ) | ||||
|                     self.log(m) | ||||
|                     self.db_rm(dbw[0], rd, fn) | ||||
| @@ -411,17 +459,22 @@ class Up2k(object): | ||||
|                     in_db = None | ||||
|  | ||||
|                 self.pp.msg = "a{} {}".format(self.pp.n, abspath) | ||||
|                 if inf.st_size > 1024 * 1024: | ||||
|                     self.log("file: {}".format(abspath)) | ||||
|  | ||||
|                 try: | ||||
|                     hashes = self._hashlist_from_file(abspath) | ||||
|                 except Exception as ex: | ||||
|                     self.log("hash: {} @ [{}]".format(repr(ex), abspath)) | ||||
|                     continue | ||||
|                 if nohash: | ||||
|                     wark = up2k_wark_from_metadata(self.salt, sz, lmod, rd, fn) | ||||
|                 else: | ||||
|                     if sz > 1024 * 1024: | ||||
|                         self.log("file: {}".format(abspath)) | ||||
|  | ||||
|                 wark = up2k_wark_from_hashlist(self.salt, inf.st_size, hashes) | ||||
|                 self.db_add(dbw[0], wark, rd, fn, lmod, inf.st_size) | ||||
|                     try: | ||||
|                         hashes = self._hashlist_from_file(abspath) | ||||
|                     except Exception as ex: | ||||
|                         self.log("hash: {} @ [{}]".format(repr(ex), abspath)) | ||||
|                         continue | ||||
|  | ||||
|                     wark = up2k_wark_from_hashlist(self.salt, sz, hashes) | ||||
|  | ||||
|                 self.db_add(dbw[0], wark, rd, fn, lmod, sz) | ||||
|                 dbw[1] += 1 | ||||
|                 ret += 1 | ||||
|                 td = time.time() - dbw[2] | ||||
| @@ -753,7 +806,9 @@ class Up2k(object): | ||||
|  | ||||
|         mpool = Queue(nw) | ||||
|         for _ in range(nw): | ||||
|             thr = threading.Thread(target=self._tag_thr, args=(mpool,)) | ||||
|             thr = threading.Thread( | ||||
|                 target=self._tag_thr, args=(mpool,), name="up2k-mpool" | ||||
|             ) | ||||
|             thr.daemon = True | ||||
|             thr.start() | ||||
|  | ||||
| @@ -838,59 +893,31 @@ class Up2k(object): | ||||
|         if not existed and ver is None: | ||||
|             return self._create_db(db_path, cur) | ||||
|  | ||||
|         orig_ver = ver | ||||
|         if not ver or ver < 3: | ||||
|             bak = "{}.bak.{:x}.v{}".format(db_path, int(time.time()), ver) | ||||
|             db = cur.connection | ||||
|             cur.close() | ||||
|             db.close() | ||||
|             msg = "creating new DB (old is bad); backup: {}" | ||||
|             if ver: | ||||
|                 msg = "creating backup before upgrade: {}" | ||||
|  | ||||
|             self.log(msg.format(bak)) | ||||
|             shutil.copy2(db_path, bak) | ||||
|             cur = self._orz(db_path) | ||||
|  | ||||
|         if ver == 1: | ||||
|             cur = self._upgrade_v1(cur, db_path) | ||||
|             if cur: | ||||
|                 ver = 2 | ||||
|  | ||||
|         if ver == 2: | ||||
|             cur = self._create_v3(cur) | ||||
|             ver = self._read_ver(cur) if cur else None | ||||
|  | ||||
|         if ver == 3: | ||||
|             if orig_ver != ver: | ||||
|                 cur.connection.commit() | ||||
|                 cur.execute("vacuum") | ||||
|                 cur.connection.commit() | ||||
|  | ||||
|         if ver == DB_VER: | ||||
|             try: | ||||
|                 nfiles = next(cur.execute("select count(w) from up"))[0] | ||||
|                 self.log("OK: {} |{}|".format(db_path, nfiles)) | ||||
|                 return cur | ||||
|             except Exception as ex: | ||||
|                 self.log("WARN: could not list files, DB corrupt?\n  " + repr(ex)) | ||||
|             except: | ||||
|                 self.log("WARN: could not list files; DB corrupt?\n" + min_ex()) | ||||
|  | ||||
|         if cur: | ||||
|             db = cur.connection | ||||
|             cur.close() | ||||
|             db.close() | ||||
|         elif ver > DB_VER: | ||||
|             m = "database is version {}, this copyparty only supports versions <= {}" | ||||
|             raise Exception(m.format(ver, DB_VER)) | ||||
|  | ||||
|         bak = "{}.bak.{:x}.v{}".format(db_path, int(time.time()), ver) | ||||
|         db = cur.connection | ||||
|         cur.close() | ||||
|         db.close() | ||||
|         msg = "creating new DB (old is bad); backup: {}" | ||||
|         if ver: | ||||
|             msg = "creating new DB (too old to upgrade); backup: {}" | ||||
|  | ||||
|         self.log(msg.format(bak)) | ||||
|         os.rename(fsenc(db_path), fsenc(bak)) | ||||
|  | ||||
|         return self._create_db(db_path, None) | ||||
|  | ||||
|     def _create_db(self, db_path, cur): | ||||
|         if not cur: | ||||
|             cur = self._orz(db_path) | ||||
|  | ||||
|         self._create_v2(cur) | ||||
|         self._create_v3(cur) | ||||
|         cur.connection.commit() | ||||
|         self.log("created DB at {}".format(db_path)) | ||||
|         return cur | ||||
|  | ||||
|     def _read_ver(self, cur): | ||||
|         for tab in ["ki", "kv"]: | ||||
|             try: | ||||
| @@ -902,69 +929,43 @@ class Up2k(object): | ||||
|             if rows: | ||||
|                 return int(rows[0][0]) | ||||
|  | ||||
|     def _create_v2(self, cur): | ||||
|         for cmd in [ | ||||
|             r"create table up (w text, mt int, sz int, rd text, fn text)", | ||||
|             r"create index up_rd on up(rd)", | ||||
|             r"create index up_fn on up(fn)", | ||||
|         ]: | ||||
|             cur.execute(cmd) | ||||
|         return cur | ||||
|  | ||||
|     def _create_v3(self, cur): | ||||
|     def _create_db(self, db_path, cur): | ||||
|         """ | ||||
|         collision in 2^(n/2) files where n = bits (6 bits/ch) | ||||
|           10*6/2 = 2^30 =       1'073'741'824, 24.1mb idx | ||||
|           10*6/2 = 2^30 =       1'073'741'824, 24.1mb idx  1<<(3*10) | ||||
|           12*6/2 = 2^36 =      68'719'476'736, 24.8mb idx | ||||
|           16*6/2 = 2^48 = 281'474'976'710'656, 26.1mb idx | ||||
|         """ | ||||
|         for c, ks in [["drop table k", "isv"], ["drop index up_", "w"]]: | ||||
|             for k in ks: | ||||
|                 try: | ||||
|                     cur.execute(c + k) | ||||
|                 except: | ||||
|                     pass | ||||
|         if not cur: | ||||
|             cur = self._orz(db_path) | ||||
|  | ||||
|         idx = r"create index up_w on up(substr(w,1,16))" | ||||
|         if self.no_expr_idx: | ||||
|             idx = r"create index up_w on up(w)" | ||||
|  | ||||
|         for cmd in [ | ||||
|             r"create table up (w text, mt int, sz int, rd text, fn text)", | ||||
|             r"create index up_rd on up(rd)", | ||||
|             r"create index up_fn on up(fn)", | ||||
|             idx, | ||||
|             r"create table mt (w text, k text, v int)", | ||||
|             r"create index mt_w on mt(w)", | ||||
|             r"create index mt_k on mt(k)", | ||||
|             r"create index mt_v on mt(v)", | ||||
|             r"create table kv (k text, v int)", | ||||
|             r"insert into kv values ('sver', 3)", | ||||
|             r"insert into kv values ('sver', {})".format(DB_VER), | ||||
|         ]: | ||||
|             cur.execute(cmd) | ||||
|  | ||||
|         cur.connection.commit() | ||||
|         self.log("created DB at {}".format(db_path)) | ||||
|         return cur | ||||
|  | ||||
|     def _upgrade_v1(self, odb, db_path): | ||||
|         npath = db_path + ".next" | ||||
|         if os.path.exists(npath): | ||||
|             os.unlink(npath) | ||||
|  | ||||
|         ndb = self._orz(npath) | ||||
|         self._create_v2(ndb) | ||||
|  | ||||
|         c = odb.execute("select * from up") | ||||
|         for wark, ts, sz, rp in c: | ||||
|             rd, fn = rp.rsplit("/", 1) if "/" in rp else ["", rp] | ||||
|             v = (wark, ts, sz, rd, fn) | ||||
|             ndb.execute("insert into up values (?,?,?,?,?)", v) | ||||
|  | ||||
|         ndb.connection.commit() | ||||
|         ndb.connection.close() | ||||
|         odb.connection.close() | ||||
|         atomic_move(npath, db_path) | ||||
|         return self._orz(db_path) | ||||
|  | ||||
|     def handle_json(self, cj): | ||||
|         if not self.register_vpath(cj["ptop"], cj["vcfg"]): | ||||
|             if cj["ptop"] not in self.registry: | ||||
|                 raise Pebkac(410, "location unavailable") | ||||
|         with self.mutex: | ||||
|             if not self.register_vpath(cj["ptop"], cj["vcfg"]): | ||||
|                 if cj["ptop"] not in self.registry: | ||||
|                     raise Pebkac(410, "location unavailable") | ||||
|  | ||||
|         cj["name"] = sanitize_fn(cj["name"], bad=[".prologue.html", ".epilogue.html"]) | ||||
|         cj["poke"] = time.time() | ||||
| @@ -972,7 +973,7 @@ class Up2k(object): | ||||
|         now = time.time() | ||||
|         job = None | ||||
|         with self.mutex: | ||||
|             cur = self.cur.get(cj["ptop"], None) | ||||
|             cur = self.cur.get(cj["ptop"]) | ||||
|             reg = self.registry[cj["ptop"]] | ||||
|             if cur: | ||||
|                 if self.no_expr_idx: | ||||
| @@ -1130,7 +1131,7 @@ class Up2k(object): | ||||
|  | ||||
|     def handle_chunk(self, ptop, wark, chash): | ||||
|         with self.mutex: | ||||
|             job = self.registry[ptop].get(wark, None) | ||||
|             job = self.registry[ptop].get(wark) | ||||
|             if not job: | ||||
|                 known = " ".join([x for x in self.registry[ptop].keys()]) | ||||
|                 self.log("unknown wark [{}], known: {}".format(wark, known)) | ||||
| @@ -1195,7 +1196,7 @@ class Up2k(object): | ||||
|         return ret, dst | ||||
|  | ||||
|     def idx_wark(self, ptop, wark, rd, fn, lmod, sz): | ||||
|         cur = self.cur.get(ptop, None) | ||||
|         cur = self.cur.get(ptop) | ||||
|         if not cur: | ||||
|             return False | ||||
|  | ||||
| @@ -1205,6 +1206,7 @@ class Up2k(object): | ||||
|  | ||||
|         if "e2t" in self.flags[ptop]: | ||||
|             self.tagq.put([ptop, wark, rd, fn]) | ||||
|             self.n_tagq += 1 | ||||
|  | ||||
|         return True | ||||
|  | ||||
| @@ -1265,9 +1267,9 @@ class Up2k(object): | ||||
|                     hashobj.update(buf) | ||||
|                     rem -= len(buf) | ||||
|  | ||||
|                 digest = hashobj.digest()[:32] | ||||
|                 digest = hashobj.digest()[:33] | ||||
|                 digest = base64.urlsafe_b64encode(digest) | ||||
|                 ret.append(digest.decode("utf-8").rstrip("=")) | ||||
|                 ret.append(digest.decode("utf-8")) | ||||
|  | ||||
|         return ret | ||||
|  | ||||
| @@ -1330,11 +1332,12 @@ class Up2k(object): | ||||
|                 for k, reg in self.registry.items(): | ||||
|                     self._snap_reg(prev, k, reg, discard_interval) | ||||
|  | ||||
|     def _snap_reg(self, prev, k, reg, discard_interval): | ||||
|     def _snap_reg(self, prev, ptop, reg, discard_interval): | ||||
|         now = time.time() | ||||
|         histpath = self.asrv.vfs.histtab[ptop] | ||||
|         rm = [x for x in reg.values() if now - x["poke"] > discard_interval] | ||||
|         if rm: | ||||
|             m = "dropping {} abandoned uploads in {}".format(len(rm), k) | ||||
|             m = "dropping {} abandoned uploads in {}".format(len(rm), ptop) | ||||
|             vis = [self._vis_job_progress(x) for x in rm] | ||||
|             self.log("\n".join([m] + vis)) | ||||
|             for job in rm: | ||||
| @@ -1352,21 +1355,21 @@ class Up2k(object): | ||||
|                 except: | ||||
|                     pass | ||||
|  | ||||
|         path = os.path.join(k, ".hist", "up2k.snap") | ||||
|         path = os.path.join(histpath, "up2k.snap") | ||||
|         if not reg: | ||||
|             if k not in prev or prev[k] is not None: | ||||
|                 prev[k] = None | ||||
|             if ptop not in prev or prev[ptop] is not None: | ||||
|                 prev[ptop] = None | ||||
|                 if os.path.exists(fsenc(path)): | ||||
|                     os.unlink(fsenc(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): | ||||
|         if etag == prev.get(ptop): | ||||
|             return | ||||
|  | ||||
|         try: | ||||
|             os.mkdir(os.path.join(k, ".hist")) | ||||
|             os.makedirs(histpath) | ||||
|         except: | ||||
|             pass | ||||
|  | ||||
| @@ -1378,14 +1381,21 @@ class Up2k(object): | ||||
|         atomic_move(path2, path) | ||||
|  | ||||
|         self.log("snap: {} |{}|".format(path, len(reg.keys()))) | ||||
|         prev[k] = etag | ||||
|         prev[ptop] = etag | ||||
|  | ||||
|     def _tagger(self): | ||||
|         with self.mutex: | ||||
|             self.n_tagq += 1 | ||||
|  | ||||
|         while True: | ||||
|             with self.mutex: | ||||
|                 self.n_tagq -= 1 | ||||
|  | ||||
|             ptop, wark, rd, fn = self.tagq.get() | ||||
|             if "e2t" not in self.flags[ptop]: | ||||
|                 continue | ||||
|  | ||||
|             # self.log("\n  " + repr([ptop, rd, fn])) | ||||
|             abspath = os.path.join(ptop, rd, fn) | ||||
|             tags = self.mtag.get(abspath) | ||||
|             ntags1 = len(tags) | ||||
| @@ -1411,8 +1421,16 @@ class Up2k(object): | ||||
|             self.log("tagged {} ({}+{})".format(abspath, ntags1, len(tags) - ntags1)) | ||||
|  | ||||
|     def _hasher(self): | ||||
|         with self.mutex: | ||||
|             self.n_hashq += 1 | ||||
|  | ||||
|         while True: | ||||
|             with self.mutex: | ||||
|                 self.n_hashq -= 1 | ||||
|             # self.log("hashq {}".format(self.n_hashq)) | ||||
|  | ||||
|             ptop, rd, fn = self.hashq.get() | ||||
|             # self.log("hashq {} pop {}/{}/{}".format(self.n_hashq, ptop, rd, fn)) | ||||
|             if "e2d" not in self.flags[ptop]: | ||||
|                 continue | ||||
|  | ||||
| @@ -1425,8 +1443,11 @@ class Up2k(object): | ||||
|                 self.idx_wark(ptop, wark, rd, fn, inf.st_mtime, inf.st_size) | ||||
|  | ||||
|     def hash_file(self, ptop, flags, rd, fn): | ||||
|         self.register_vpath(ptop, flags) | ||||
|         self.hashq.put([ptop, rd, fn]) | ||||
|         with self.mutex: | ||||
|             self.register_vpath(ptop, flags) | ||||
|             self.hashq.put([ptop, rd, fn]) | ||||
|             self.n_hashq += 1 | ||||
|         # self.log("hashq {} push {}/{}/{}".format(self.n_hashq, ptop, rd, fn)) | ||||
|  | ||||
|  | ||||
| def up2k_chunksize(filesize): | ||||
| @@ -1448,9 +1469,12 @@ def up2k_wark_from_hashlist(salt, filesize, hashes): | ||||
|     ident.extend(hashes) | ||||
|     ident = "\n".join(ident) | ||||
|  | ||||
|     hasher = hashlib.sha512() | ||||
|     hasher.update(ident.encode("utf-8")) | ||||
|     digest = hasher.digest()[:32] | ||||
|     wark = hashlib.sha512(ident.encode("utf-8")).digest()[:33] | ||||
|     wark = base64.urlsafe_b64encode(wark) | ||||
|     return wark.decode("ascii") | ||||
|  | ||||
|     wark = base64.urlsafe_b64encode(digest) | ||||
|     return wark.decode("utf-8").rstrip("=") | ||||
|  | ||||
| def up2k_wark_from_metadata(salt, sz, lastmod, rd, fn): | ||||
|     ret = fsenc("{}\n{}\n{}\n{}\n{}".format(salt, lastmod, sz, rd, fn)) | ||||
|     ret = base64.urlsafe_b64encode(hashlib.sha512(ret).digest()) | ||||
|     return "#{}".format(ret.decode("ascii"))[:44] | ||||
|   | ||||
| @@ -193,7 +193,7 @@ class ProgressPrinter(threading.Thread): | ||||
|     """ | ||||
|  | ||||
|     def __init__(self): | ||||
|         threading.Thread.__init__(self) | ||||
|         threading.Thread.__init__(self, name="pp") | ||||
|         self.daemon = True | ||||
|         self.msg = None | ||||
|         self.end = False | ||||
| @@ -208,6 +208,8 @@ class ProgressPrinter(threading.Thread): | ||||
|  | ||||
|             msg = self.msg | ||||
|             uprint(" {}\033[K\r".format(msg)) | ||||
|             if PY2: | ||||
|                 sys.stdout.flush() | ||||
|  | ||||
|         print("\033[K", end="") | ||||
|         sys.stdout.flush()  # necessary on win10 even w/ stderr btw | ||||
| @@ -252,6 +254,45 @@ def trace(*args, **kwargs): | ||||
|     nuprint(msg) | ||||
|  | ||||
|  | ||||
| def alltrace(): | ||||
|     threads = {} | ||||
|     names = dict([(t.ident, t.name) for t in threading.enumerate()]) | ||||
|     for tid, stack in sys._current_frames().items(): | ||||
|         name = "{} ({:x})".format(names.get(tid), tid) | ||||
|         threads[name] = stack | ||||
|  | ||||
|     rret = [] | ||||
|     bret = [] | ||||
|     for name, stack in sorted(threads.items()): | ||||
|         ret = ["\n\n# {}".format(name)] | ||||
|         pad = None | ||||
|         for fn, lno, name, line in traceback.extract_stack(stack): | ||||
|             fn = os.sep.join(fn.split(os.sep)[-3:]) | ||||
|             ret.append('File: "{}", line {}, in {}'.format(fn, lno, name)) | ||||
|             if line: | ||||
|                 ret.append("  " + str(line.strip())) | ||||
|                 if "self.not_empty.wait()" in line: | ||||
|                     pad = " " * 4 | ||||
|  | ||||
|         if pad: | ||||
|             bret += [ret[0]] + [pad + x for x in ret[1:]] | ||||
|         else: | ||||
|             rret += ret | ||||
|  | ||||
|     return "\n".join(rret + bret) | ||||
|  | ||||
|  | ||||
| def min_ex(): | ||||
|     et, ev, tb = sys.exc_info() | ||||
|     tb = traceback.extract_tb(tb, 2) | ||||
|     ex = [ | ||||
|         "{} @ {} <{}>: {}".format(fp.split(os.sep)[-1], ln, fun, txt) | ||||
|         for fp, ln, fun, txt in tb | ||||
|     ] | ||||
|     ex.append("{}: {}".format(et.__name__, ev)) | ||||
|     return "\n".join(ex) | ||||
|  | ||||
|  | ||||
| @contextlib.contextmanager | ||||
| def ren_open(fname, *args, **kwargs): | ||||
|     fdir = kwargs.pop("fdir", None) | ||||
| @@ -310,7 +351,7 @@ def ren_open(fname, *args, **kwargs): | ||||
|         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("=") | ||||
|             b64 = base64.urlsafe_b64encode(b64).decode("utf-8") | ||||
|  | ||||
|         badlen = len(fname) | ||||
|         while len(fname) >= badlen: | ||||
| @@ -566,8 +607,10 @@ def read_header(sr): | ||||
|             else: | ||||
|                 continue | ||||
|  | ||||
|         sr.unrecv(ret[ofs + 4 :]) | ||||
|         return ret[:ofs].decode("utf-8", "surrogateescape").split("\r\n") | ||||
|         if len(ret) > ofs + 4: | ||||
|             sr.unrecv(ret[ofs + 4 :]) | ||||
|  | ||||
|         return ret[:ofs].decode("utf-8", "surrogateescape").lstrip("\r\n").split("\r\n") | ||||
|  | ||||
|  | ||||
| def humansize(sz, terse=False): | ||||
| @@ -605,6 +648,16 @@ def s2hms(s, optional_h=False): | ||||
|     return "{}:{:02}:{:02}".format(h, m, s) | ||||
|  | ||||
|  | ||||
| def uncyg(path): | ||||
|     if len(path) < 2 or not path.startswith("/"): | ||||
|         return path | ||||
|  | ||||
|     if len(path) > 2 and path[2] != "/": | ||||
|         return path | ||||
|  | ||||
|     return "{}:\\{}".format(path[1], path[3:]) | ||||
|  | ||||
|  | ||||
| def undot(path): | ||||
|     ret = [] | ||||
|     for node in path.split("/"): | ||||
| @@ -852,30 +905,36 @@ def yieldfile(fn): | ||||
|  | ||||
|  | ||||
| def hashcopy(actor, fin, fout): | ||||
|     u32_lim = int((2 ** 31) * 0.9) | ||||
|     is_mp = actor.is_mp | ||||
|     hashobj = hashlib.sha512() | ||||
|     tlen = 0 | ||||
|     for buf in fin: | ||||
|         actor.workload += 1 | ||||
|         if actor.workload > u32_lim: | ||||
|             actor.workload = 100  # prevent overflow | ||||
|         if is_mp: | ||||
|             actor.workload += 1 | ||||
|             if actor.workload > 2 ** 31: | ||||
|                 actor.workload = 100 | ||||
|  | ||||
|         tlen += len(buf) | ||||
|         hashobj.update(buf) | ||||
|         fout.write(buf) | ||||
|  | ||||
|     digest32 = hashobj.digest()[:32] | ||||
|     digest_b64 = base64.urlsafe_b64encode(digest32).decode("utf-8").rstrip("=") | ||||
|     digest = hashobj.digest()[:33] | ||||
|     digest_b64 = base64.urlsafe_b64encode(digest).decode("utf-8") | ||||
|  | ||||
|     return tlen, hashobj.hexdigest(), digest_b64 | ||||
|  | ||||
|  | ||||
| def sendfile_py(lower, upper, f, s): | ||||
| def sendfile_py(lower, upper, f, s, actor=None): | ||||
|     remains = upper - lower | ||||
|     f.seek(lower) | ||||
|     while remains > 0: | ||||
|         if actor: | ||||
|             actor.workload += 1 | ||||
|             if actor.workload > 2 ** 31: | ||||
|                 actor.workload = 100 | ||||
|  | ||||
|         # time.sleep(0.01) | ||||
|         buf = f.read(min(4096, remains)) | ||||
|         buf = f.read(min(1024 * 32, remains)) | ||||
|         if not buf: | ||||
|             return remains | ||||
|  | ||||
| @@ -977,8 +1036,8 @@ def guess_mime(url, fallback="application/octet-stream"): | ||||
| 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") | ||||
|     stdout = stdout.decode("utf-8", "replace") | ||||
|     stderr = stderr.decode("utf-8", "replace") | ||||
|     return [p.returncode, stdout, stderr] | ||||
|  | ||||
|  | ||||
|   | ||||
							
								
								
									
										583
									
								
								copyparty/web/baguettebox.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										583
									
								
								copyparty/web/baguettebox.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,583 @@ | ||||
| /*! | ||||
|  * baguetteBox.js | ||||
|  * @author  feimosi | ||||
|  * @version 1.11.1-mod | ||||
|  * @url https://github.com/feimosi/baguetteBox.js | ||||
|  */ | ||||
|  | ||||
| window.baguetteBox = (function () { | ||||
|     'use strict'; | ||||
|  | ||||
|     var options = {}, | ||||
|         defaults = { | ||||
|             captions: true, | ||||
|             buttons: 'auto', | ||||
|             noScrollbars: false, | ||||
|             bodyClass: 'baguetteBox-open', | ||||
|             titleTag: false, | ||||
|             async: false, | ||||
|             preload: 2, | ||||
|             animation: 'slideIn', | ||||
|             afterShow: null, | ||||
|             afterHide: null, | ||||
|             onChange: null, | ||||
|         }, | ||||
|         overlay, slider, previousButton, nextButton, closeButton, | ||||
|         currentGallery = [], | ||||
|         currentIndex = 0, | ||||
|         isOverlayVisible = false, | ||||
|         touch = {},  // start-pos | ||||
|         touchFlag = false,  // busy | ||||
|         regex = /.+\.(gif|jpe?g|png|webp)/i, | ||||
|         data = {},  // all galleries | ||||
|         imagesElements = [], | ||||
|         documentLastFocus = null; | ||||
|  | ||||
|     var overlayClickHandler = function (event) { | ||||
|         if (event.target.id.indexOf('baguette-img') !== -1) { | ||||
|             hideOverlay(); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     var touchstartHandler = function (event) { | ||||
|         touch.count++; | ||||
|         if (touch.count > 1) { | ||||
|             touch.multitouch = true; | ||||
|         } | ||||
|         touch.startX = event.changedTouches[0].pageX; | ||||
|         touch.startY = event.changedTouches[0].pageY; | ||||
|     }; | ||||
|     var touchmoveHandler = function (event) { | ||||
|         if (touchFlag || touch.multitouch) { | ||||
|             return; | ||||
|         } | ||||
|         event.preventDefault ? event.preventDefault() : event.returnValue = false; | ||||
|         var touchEvent = event.touches[0] || event.changedTouches[0]; | ||||
|         if (touchEvent.pageX - touch.startX > 40) { | ||||
|             touchFlag = true; | ||||
|             showPreviousImage(); | ||||
|         } else if (touchEvent.pageX - touch.startX < -40) { | ||||
|             touchFlag = true; | ||||
|             showNextImage(); | ||||
|         } else if (touch.startY - touchEvent.pageY > 100) { | ||||
|             hideOverlay(); | ||||
|         } | ||||
|     }; | ||||
|     var touchendHandler = function () { | ||||
|         touch.count--; | ||||
|         if (touch.count <= 0) { | ||||
|             touch.multitouch = false; | ||||
|         } | ||||
|         touchFlag = false; | ||||
|     }; | ||||
|     var contextmenuHandler = function () { | ||||
|         touchendHandler(); | ||||
|     }; | ||||
|  | ||||
|     var trapFocusInsideOverlay = function (event) { | ||||
|         if (overlay.style.display === 'block' && (overlay.contains && !overlay.contains(event.target))) { | ||||
|             event.stopPropagation(); | ||||
|             initFocus(); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     function run(selector, userOptions) { | ||||
|         buildOverlay(); | ||||
|         removeFromCache(selector); | ||||
|         return bindImageClickListeners(selector, userOptions); | ||||
|     } | ||||
|  | ||||
|     function bindImageClickListeners(selector, userOptions) { | ||||
|         var galleryNodeList = document.querySelectorAll(selector); | ||||
|         var selectorData = { | ||||
|             galleries: [], | ||||
|             nodeList: galleryNodeList | ||||
|         }; | ||||
|         data[selector] = selectorData; | ||||
|  | ||||
|         [].forEach.call(galleryNodeList, function (galleryElement) { | ||||
|             if (userOptions && userOptions.filter) { | ||||
|                 regex = userOptions.filter; | ||||
|             } | ||||
|  | ||||
|             var tagsNodeList = []; | ||||
|             if (galleryElement.tagName === 'A') { | ||||
|                 tagsNodeList = [galleryElement]; | ||||
|             } else { | ||||
|                 tagsNodeList = galleryElement.getElementsByTagName('a'); | ||||
|             } | ||||
|  | ||||
|             tagsNodeList = [].filter.call(tagsNodeList, function (element) { | ||||
|                 if (element.className.indexOf(userOptions && userOptions.ignoreClass) === -1) { | ||||
|                     return regex.test(element.href); | ||||
|                 } | ||||
|             }); | ||||
|             if (tagsNodeList.length === 0) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             var gallery = []; | ||||
|             [].forEach.call(tagsNodeList, function (imageElement, imageIndex) { | ||||
|                 var imageElementClickHandler = function (event) { | ||||
|                     if (event && event.ctrlKey) | ||||
|                         return true; | ||||
|  | ||||
|                     event.preventDefault ? event.preventDefault() : event.returnValue = false; | ||||
|                     prepareOverlay(gallery, userOptions); | ||||
|                     showOverlay(imageIndex); | ||||
|                 }; | ||||
|                 var imageItem = { | ||||
|                     eventHandler: imageElementClickHandler, | ||||
|                     imageElement: imageElement | ||||
|                 }; | ||||
|                 bind(imageElement, 'click', imageElementClickHandler); | ||||
|                 gallery.push(imageItem); | ||||
|             }); | ||||
|             selectorData.galleries.push(gallery); | ||||
|         }); | ||||
|  | ||||
|         return selectorData.galleries; | ||||
|     } | ||||
|  | ||||
|     function clearCachedData() { | ||||
|         for (var selector in data) { | ||||
|             if (data.hasOwnProperty(selector)) { | ||||
|                 removeFromCache(selector); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function removeFromCache(selector) { | ||||
|         if (!data.hasOwnProperty(selector)) { | ||||
|             return; | ||||
|         } | ||||
|         var galleries = data[selector].galleries; | ||||
|         [].forEach.call(galleries, function (gallery) { | ||||
|             [].forEach.call(gallery, function (imageItem) { | ||||
|                 unbind(imageItem.imageElement, 'click', imageItem.eventHandler); | ||||
|             }); | ||||
|  | ||||
|             if (currentGallery === gallery) { | ||||
|                 currentGallery = []; | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         delete data[selector]; | ||||
|     } | ||||
|  | ||||
|     function buildOverlay() { | ||||
|         overlay = ebi('baguetteBox-overlay'); | ||||
|         if (overlay) { | ||||
|             slider = ebi('baguetteBox-slider'); | ||||
|             previousButton = ebi('previous-button'); | ||||
|             nextButton = ebi('next-button'); | ||||
|             closeButton = ebi('close-button'); | ||||
|             return; | ||||
|         } | ||||
|         overlay = mknod('div'); | ||||
|         overlay.setAttribute('role', 'dialog'); | ||||
|         overlay.id = 'baguetteBox-overlay'; | ||||
|         document.getElementsByTagName('body')[0].appendChild(overlay); | ||||
|  | ||||
|         slider = mknod('div'); | ||||
|         slider.id = 'baguetteBox-slider'; | ||||
|         overlay.appendChild(slider); | ||||
|  | ||||
|         previousButton = mknod('button'); | ||||
|         previousButton.setAttribute('type', 'button'); | ||||
|         previousButton.id = 'previous-button'; | ||||
|         previousButton.setAttribute('aria-label', 'Previous'); | ||||
|         previousButton.innerHTML = '<'; | ||||
|         overlay.appendChild(previousButton); | ||||
|  | ||||
|         nextButton = mknod('button'); | ||||
|         nextButton.setAttribute('type', 'button'); | ||||
|         nextButton.id = 'next-button'; | ||||
|         nextButton.setAttribute('aria-label', 'Next'); | ||||
|         nextButton.innerHTML = '>'; | ||||
|         overlay.appendChild(nextButton); | ||||
|  | ||||
|         closeButton = mknod('button'); | ||||
|         closeButton.setAttribute('type', 'button'); | ||||
|         closeButton.id = 'close-button'; | ||||
|         closeButton.setAttribute('aria-label', 'Close'); | ||||
|         closeButton.innerHTML = '×'; | ||||
|         overlay.appendChild(closeButton); | ||||
|  | ||||
|         previousButton.className = nextButton.className = closeButton.className = 'baguetteBox-button'; | ||||
|  | ||||
|         bindEvents(); | ||||
|     } | ||||
|  | ||||
|     function keyDownHandler(event) { | ||||
|         switch (event.keyCode) { | ||||
|             case 37: // Left | ||||
|                 showPreviousImage(); | ||||
|                 break; | ||||
|             case 39: // Right | ||||
|                 showNextImage(); | ||||
|                 break; | ||||
|             case 27: // Esc | ||||
|                 hideOverlay(); | ||||
|                 break; | ||||
|             case 36: // Home | ||||
|                 showFirstImage(event); | ||||
|                 break; | ||||
|             case 35: // End | ||||
|                 showLastImage(event); | ||||
|                 break; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     var passiveSupp = false; | ||||
|     try { | ||||
|         var opts = { | ||||
|             get passive() { | ||||
|                 passiveSupp = true; | ||||
|                 return false; | ||||
|             } | ||||
|         }; | ||||
|         window.addEventListener('test', null, opts); | ||||
|         window.removeEventListener('test', null, opts); | ||||
|     } | ||||
|     catch (ex) { | ||||
|         passiveSupp = false; | ||||
|     } | ||||
|     var passiveEvent = passiveSupp ? { passive: false } : null; | ||||
|     var nonPassiveEvent = passiveSupp ? { passive: true } : null; | ||||
|  | ||||
|     function bindEvents() { | ||||
|         bind(overlay, 'click', overlayClickHandler); | ||||
|         bind(previousButton, 'click', showPreviousImage); | ||||
|         bind(nextButton, 'click', showNextImage); | ||||
|         bind(closeButton, 'click', hideOverlay); | ||||
|         bind(slider, 'contextmenu', contextmenuHandler); | ||||
|         bind(overlay, 'touchstart', touchstartHandler, nonPassiveEvent); | ||||
|         bind(overlay, 'touchmove', touchmoveHandler, passiveEvent); | ||||
|         bind(overlay, 'touchend', touchendHandler); | ||||
|         bind(document, 'focus', trapFocusInsideOverlay, true); | ||||
|     } | ||||
|  | ||||
|     function unbindEvents() { | ||||
|         unbind(overlay, 'click', overlayClickHandler); | ||||
|         unbind(previousButton, 'click', showPreviousImage); | ||||
|         unbind(nextButton, 'click', showNextImage); | ||||
|         unbind(closeButton, 'click', hideOverlay); | ||||
|         unbind(slider, 'contextmenu', contextmenuHandler); | ||||
|         unbind(overlay, 'touchstart', touchstartHandler, nonPassiveEvent); | ||||
|         unbind(overlay, 'touchmove', touchmoveHandler, passiveEvent); | ||||
|         unbind(overlay, 'touchend', touchendHandler); | ||||
|         unbind(document, 'focus', trapFocusInsideOverlay, true); | ||||
|     } | ||||
|  | ||||
|     function prepareOverlay(gallery, userOptions) { | ||||
|         if (currentGallery === gallery) { | ||||
|             return; | ||||
|         } | ||||
|         currentGallery = gallery; | ||||
|         setOptions(userOptions); | ||||
|         slider.innerHTML = ''; | ||||
|         imagesElements.length = 0; | ||||
|  | ||||
|         var imagesFiguresIds = []; | ||||
|         var imagesCaptionsIds = []; | ||||
|         for (var i = 0, fullImage; i < gallery.length; i++) { | ||||
|             fullImage = mknod('div'); | ||||
|             fullImage.className = 'full-image'; | ||||
|             fullImage.id = 'baguette-img-' + i; | ||||
|             imagesElements.push(fullImage); | ||||
|  | ||||
|             imagesFiguresIds.push('baguetteBox-figure-' + i); | ||||
|             imagesCaptionsIds.push('baguetteBox-figcaption-' + i); | ||||
|             slider.appendChild(imagesElements[i]); | ||||
|         } | ||||
|         overlay.setAttribute('aria-labelledby', imagesFiguresIds.join(' ')); | ||||
|         overlay.setAttribute('aria-describedby', imagesCaptionsIds.join(' ')); | ||||
|     } | ||||
|  | ||||
|     function setOptions(newOptions) { | ||||
|         if (!newOptions) { | ||||
|             newOptions = {}; | ||||
|         } | ||||
|         for (var item in defaults) { | ||||
|             options[item] = defaults[item]; | ||||
|             if (typeof newOptions[item] !== 'undefined') { | ||||
|                 options[item] = newOptions[item]; | ||||
|             } | ||||
|         } | ||||
|         slider.style.transition = (options.animation === 'fadeIn' ? 'opacity .4s ease' : | ||||
|             options.animation === 'slideIn' ? '' : 'none'); | ||||
|  | ||||
|         if (options.buttons === 'auto' && ('ontouchstart' in window || currentGallery.length === 1)) { | ||||
|             options.buttons = false; | ||||
|         } | ||||
|  | ||||
|         previousButton.style.display = nextButton.style.display = (options.buttons ? '' : 'none'); | ||||
|     } | ||||
|  | ||||
|     function showOverlay(chosenImageIndex) { | ||||
|         if (options.noScrollbars) { | ||||
|             document.documentElement.style.overflowY = 'hidden'; | ||||
|             document.body.style.overflowY = 'scroll'; | ||||
|         } | ||||
|         if (overlay.style.display === 'block') { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         bind(document, 'keydown', keyDownHandler); | ||||
|         currentIndex = chosenImageIndex; | ||||
|         touch = { | ||||
|             count: 0, | ||||
|             startX: null, | ||||
|             startY: null | ||||
|         }; | ||||
|         loadImage(currentIndex, function () { | ||||
|             preloadNext(currentIndex); | ||||
|             preloadPrev(currentIndex); | ||||
|         }); | ||||
|  | ||||
|         updateOffset(); | ||||
|         overlay.style.display = 'block'; | ||||
|         // Fade in overlay | ||||
|         setTimeout(function () { | ||||
|             overlay.className = 'visible'; | ||||
|             if (options.bodyClass && document.body.classList) { | ||||
|                 document.body.classList.add(options.bodyClass); | ||||
|             } | ||||
|             if (options.afterShow) { | ||||
|                 options.afterShow(); | ||||
|             } | ||||
|         }, 50); | ||||
|         if (options.onChange) { | ||||
|             options.onChange(currentIndex, imagesElements.length); | ||||
|         } | ||||
|         documentLastFocus = document.activeElement; | ||||
|         initFocus(); | ||||
|         isOverlayVisible = true; | ||||
|     } | ||||
|  | ||||
|     function initFocus() { | ||||
|         if (options.buttons) { | ||||
|             previousButton.focus(); | ||||
|         } else { | ||||
|             closeButton.focus(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function hideOverlay(e) { | ||||
|         ev(e); | ||||
|         if (options.noScrollbars) { | ||||
|             document.documentElement.style.overflowY = 'auto'; | ||||
|             document.body.style.overflowY = 'auto'; | ||||
|         } | ||||
|         if (overlay.style.display === 'none') { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         unbind(document, 'keydown', keyDownHandler); | ||||
|         // Fade out and hide the overlay | ||||
|         overlay.className = ''; | ||||
|         setTimeout(function () { | ||||
|             overlay.style.display = 'none'; | ||||
|             if (options.bodyClass && document.body.classList) { | ||||
|                 document.body.classList.remove(options.bodyClass); | ||||
|             } | ||||
|             if (options.afterHide) { | ||||
|                 options.afterHide(); | ||||
|             } | ||||
|             documentLastFocus && documentLastFocus.focus(); | ||||
|             isOverlayVisible = false; | ||||
|         }, 500); | ||||
|     } | ||||
|  | ||||
|     function loadImage(index, callback) { | ||||
|         var imageContainer = imagesElements[index]; | ||||
|         var galleryItem = currentGallery[index]; | ||||
|  | ||||
|         if (typeof imageContainer === 'undefined' || typeof galleryItem === 'undefined') { | ||||
|             return;  // out-of-bounds or gallery dirty | ||||
|         } | ||||
|  | ||||
|         if (imageContainer.getElementsByTagName('img')[0]) { | ||||
|             // image is loaded, cb and bail | ||||
|             if (callback) { | ||||
|                 callback(); | ||||
|             } | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         var imageElement = galleryItem.imageElement, | ||||
|             imageSrc = imageElement.href, | ||||
|             thumbnailElement = imageElement.getElementsByTagName('img')[0], | ||||
|             imageCaption = typeof options.captions === 'function' ? | ||||
|                 options.captions.call(currentGallery, imageElement) : | ||||
|                 imageElement.getAttribute('data-caption') || imageElement.title; | ||||
|  | ||||
|         var figure = mknod('figure'); | ||||
|         figure.id = 'baguetteBox-figure-' + index; | ||||
|         figure.innerHTML = '<div class="baguetteBox-spinner">' + | ||||
|             '<div class="baguetteBox-double-bounce1"></div>' + | ||||
|             '<div class="baguetteBox-double-bounce2"></div>' + | ||||
|             '</div>'; | ||||
|  | ||||
|         if (options.captions && imageCaption) { | ||||
|             var figcaption = mknod('figcaption'); | ||||
|             figcaption.id = 'baguetteBox-figcaption-' + index; | ||||
|             figcaption.innerHTML = imageCaption; | ||||
|             figure.appendChild(figcaption); | ||||
|         } | ||||
|         imageContainer.appendChild(figure); | ||||
|  | ||||
|         var image = mknod('img'); | ||||
|         image.onload = function () { | ||||
|             // Remove loader element | ||||
|             var spinner = document.querySelector('#baguette-img-' + index + ' .baguetteBox-spinner'); | ||||
|             figure.removeChild(spinner); | ||||
|             if (!options.async && callback) { | ||||
|                 callback(); | ||||
|             } | ||||
|         }; | ||||
|         image.setAttribute('src', imageSrc); | ||||
|         image.alt = thumbnailElement ? thumbnailElement.alt || '' : ''; | ||||
|         if (options.titleTag && imageCaption) { | ||||
|             image.title = imageCaption; | ||||
|         } | ||||
|         figure.appendChild(image); | ||||
|  | ||||
|         if (options.async && callback) { | ||||
|             callback(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function showNextImage(e) { | ||||
|         ev(e); | ||||
|         return show(currentIndex + 1); | ||||
|     } | ||||
|  | ||||
|     function showPreviousImage(e) { | ||||
|         ev(e); | ||||
|         return show(currentIndex - 1); | ||||
|     } | ||||
|  | ||||
|     function showFirstImage(event) { | ||||
|         if (event) { | ||||
|             event.preventDefault(); | ||||
|         } | ||||
|         return show(0); | ||||
|     } | ||||
|  | ||||
|     function showLastImage(event) { | ||||
|         if (event) { | ||||
|             event.preventDefault(); | ||||
|         } | ||||
|         return show(currentGallery.length - 1); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Move the gallery to a specific index | ||||
|      * @param `index` {number} - the position of the image | ||||
|      * @param `gallery` {array} - gallery which should be opened, if omitted assumes the currently opened one | ||||
|      * @return {boolean} - true on success or false if the index is invalid | ||||
|      */ | ||||
|     function show(index, gallery) { | ||||
|         if (!isOverlayVisible && index >= 0 && index < gallery.length) { | ||||
|             prepareOverlay(gallery, options); | ||||
|             showOverlay(index); | ||||
|             return true; | ||||
|         } | ||||
|         if (index < 0) { | ||||
|             if (options.animation) { | ||||
|                 bounceAnimation('left'); | ||||
|             } | ||||
|             return false; | ||||
|         } | ||||
|         if (index >= imagesElements.length) { | ||||
|             if (options.animation) { | ||||
|                 bounceAnimation('right'); | ||||
|             } | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         currentIndex = index; | ||||
|         loadImage(currentIndex, function () { | ||||
|             preloadNext(currentIndex); | ||||
|             preloadPrev(currentIndex); | ||||
|         }); | ||||
|         updateOffset(); | ||||
|  | ||||
|         if (options.onChange) { | ||||
|             options.onChange(currentIndex, imagesElements.length); | ||||
|         } | ||||
|  | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Triggers the bounce animation | ||||
|      * @param {('left'|'right')} direction - Direction of the movement | ||||
|      */ | ||||
|     function bounceAnimation(direction) { | ||||
|         slider.className = 'bounce-from-' + direction; | ||||
|         setTimeout(function () { | ||||
|             slider.className = ''; | ||||
|         }, 400); | ||||
|     } | ||||
|  | ||||
|     function updateOffset() { | ||||
|         var offset = -currentIndex * 100 + '%'; | ||||
|         if (options.animation === 'fadeIn') { | ||||
|             slider.style.opacity = 0; | ||||
|             setTimeout(function () { | ||||
|                 slider.style.transform = 'translate3d(' + offset + ',0,0)'; | ||||
|                 slider.style.opacity = 1; | ||||
|             }, 400); | ||||
|         } else { | ||||
|             slider.style.transform = 'translate3d(' + offset + ',0,0)'; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function preloadNext(index) { | ||||
|         if (index - currentIndex >= options.preload) { | ||||
|             return; | ||||
|         } | ||||
|         loadImage(index + 1, function () { | ||||
|             preloadNext(index + 1); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     function preloadPrev(index) { | ||||
|         if (currentIndex - index >= options.preload) { | ||||
|             return; | ||||
|         } | ||||
|         loadImage(index - 1, function () { | ||||
|             preloadPrev(index - 1); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     function bind(element, event, callback, options) { | ||||
|         element.addEventListener(event, callback, options); | ||||
|     } | ||||
|  | ||||
|     function unbind(element, event, callback, options) { | ||||
|         element.removeEventListener(event, callback, options); | ||||
|     } | ||||
|  | ||||
|     function destroyPlugin() { | ||||
|         unbindEvents(); | ||||
|         clearCachedData(); | ||||
|         unbind(document, 'keydown', keyDownHandler); | ||||
|         document.getElementsByTagName('body')[0].removeChild(ebi('baguetteBox-overlay')); | ||||
|         data = {}; | ||||
|         currentGallery = []; | ||||
|         currentIndex = 0; | ||||
|     } | ||||
|  | ||||
|     return { | ||||
|         run: run, | ||||
|         show: show, | ||||
|         showNext: showNextImage, | ||||
|         showPrevious: showPreviousImage, | ||||
|         hide: hideOverlay, | ||||
|         destroy: destroyPlugin | ||||
|     }; | ||||
| })(); | ||||
| @@ -25,6 +25,35 @@ html, body { | ||||
| body { | ||||
| 	padding-bottom: 5em; | ||||
| } | ||||
| #tt { | ||||
| 	position: fixed; | ||||
| 	max-width: 34em; | ||||
| 	background: #222; | ||||
| 	border: 0 solid #555; | ||||
| 	overflow: hidden; | ||||
| 	margin-top: 1em; | ||||
| 	padding: 0 1em; | ||||
| 	height: 0; | ||||
| 	opacity: .1; | ||||
| 	transition: opacity 0.14s, height 0.14s, padding 0.14s; | ||||
| 	box-shadow: 0 .2em .5em #222; | ||||
| 	border-radius: .4em; | ||||
| 	z-index: 9001; | ||||
| } | ||||
| #tt.show { | ||||
| 	padding: 1em; | ||||
| 	height: auto; | ||||
| 	border-width: .2em 0; | ||||
| 	opacity: 1; | ||||
| } | ||||
| #tt code { | ||||
| 	background: #3c3c3c; | ||||
| 	padding: .2em .3em; | ||||
| 	border-top: 1px solid #777; | ||||
| 	border-radius: .3em; | ||||
| 	font-family: monospace, monospace; | ||||
| 	line-height: 2em; | ||||
| } | ||||
| #path, | ||||
| #path * { | ||||
| 	font-size: 1em; | ||||
| @@ -53,6 +82,7 @@ body { | ||||
| #files tbody a { | ||||
| 	display: block; | ||||
| 	padding: .3em 0; | ||||
| 	scroll-margin-top: 45vh; | ||||
| } | ||||
| #files tbody div a { | ||||
| 	color: #f5a; | ||||
| @@ -68,7 +98,6 @@ a, #files tbody div a:last-child { | ||||
| 	text-decoration: underline; | ||||
| } | ||||
| #files thead { | ||||
| 	background: #333; | ||||
| 	position: sticky; | ||||
| 	top: 0; | ||||
| } | ||||
| @@ -76,29 +105,30 @@ a, #files tbody div a:last-child { | ||||
| 	color: #999; | ||||
| 	font-weight: normal; | ||||
| } | ||||
| #files tr:hover { | ||||
| #files tr:hover td { | ||||
| 	background: #1c1c1c; | ||||
| } | ||||
| #files thead th { | ||||
| 	padding: .5em 1.3em .3em 1.3em; | ||||
| 	padding: .5em .3em .3em .3em; | ||||
| 	border-right: 2px solid #3c3c3c; | ||||
| 	border-bottom: 2px solid #444; | ||||
| 	background: #333; | ||||
| 	cursor: pointer; | ||||
| } | ||||
| #files thead th+th { | ||||
| 	border-left: 2px solid #2a2a2a; | ||||
| } | ||||
| #files thead th:last-child { | ||||
| 	background: #444; | ||||
| 	border-radius: .7em .7em 0 0; | ||||
| 	border-right: none; | ||||
| } | ||||
| #files thead th:first-child { | ||||
| #files tbody { | ||||
| 	background: #222; | ||||
| } | ||||
| #files tbody, | ||||
| #files thead th:nth-child(2) { | ||||
| 	background: #222; | ||||
| 	border-radius: 0 .7em 0 0; | ||||
| } | ||||
| #files td { | ||||
| 	margin: 0; | ||||
| 	padding: 0 .5em; | ||||
| 	border-bottom: 1px solid #111; | ||||
| 	border-left: 1px solid #2c2c2c; | ||||
| } | ||||
| #files td+td+td { | ||||
| 	max-width: 30em; | ||||
| @@ -185,9 +215,17 @@ a, #files tbody div a:last-child { | ||||
| 	margin: -.2em; | ||||
| } | ||||
| #files tbody a.play.act { | ||||
| 	color: #840; | ||||
| 	color: #720; | ||||
| 	text-shadow: 0 0 .3em #b80; | ||||
| } | ||||
| #ggrid a.play, | ||||
| html.light #ggrid a.play { | ||||
| 	color: #fff; | ||||
| 	background: #750; | ||||
| 	border-color: #c90; | ||||
| 	border-top: 1px solid #da4; | ||||
| 	box-shadow: 0 .1em 1.2em #b83; | ||||
| } | ||||
| #files tbody tr.sel td, | ||||
| #ggrid a.sel, | ||||
| html.light #ggrid a.sel { | ||||
| @@ -209,11 +247,17 @@ html.light #ggrid a.sel { | ||||
| 	box-shadow: 0 .1em 1.2em #b36; | ||||
| 	transition: all 0.2s cubic-bezier(.2, 2.2, .5, 1); /* https://cubic-bezier.com/#.4,2,.7,1 */ | ||||
| } | ||||
| #ggrid a.sel img { | ||||
| #ggrid a.sel img, | ||||
| #ggrid a.play img { | ||||
| 	opacity: .7; | ||||
| 	box-shadow: 0 0 1em #b36; | ||||
| 	filter: contrast(130%) brightness(107%); | ||||
| } | ||||
| #ggrid a.sel img { | ||||
| 	box-shadow: 0 0 1em #b36; | ||||
| } | ||||
| #ggrid a.play img { | ||||
| 	box-shadow: 0 0 1em #b83; | ||||
| } | ||||
| #files tr.sel a { | ||||
| 	color: #fff; | ||||
| } | ||||
| @@ -267,6 +311,7 @@ html.light #ggrid a.sel { | ||||
| 	height: 6em; | ||||
| 	width: 100%; | ||||
| 	z-index: 3; | ||||
| 	touch-action: none; | ||||
| 	transition: bottom 0.15s; | ||||
| } | ||||
| #widget.open { | ||||
| @@ -483,20 +528,56 @@ html.light #ggrid a.sel { | ||||
| 	margin: .5em; | ||||
| } | ||||
| .opview input[type=text] { | ||||
| 	color: #fff; | ||||
| 	background: #383838; | ||||
| 	color: #fff; | ||||
| 	border: none; | ||||
| 	box-shadow: 0 0 .3em #222; | ||||
| 	border-bottom: 1px solid #fc5; | ||||
| 	border-radius: .2em; | ||||
| 	padding: .2em .3em; | ||||
| } | ||||
| .opview input.err, | ||||
| html.light .opview input[type="text"].err { | ||||
| 	color: #fff; | ||||
| 	background: #a20; | ||||
| 	border-color: #f00; | ||||
| 	box-shadow: 0 0 .7em #f00; | ||||
| 	text-shadow: 1px 1px 0 #500; | ||||
| 	outline: none; | ||||
| } | ||||
| input[type="checkbox"]+label { | ||||
| 	color: #f5a; | ||||
| } | ||||
| input[type="checkbox"]:checked+label { | ||||
| 	color: #fc5; | ||||
| } | ||||
| input[type="radio"]:checked+label { | ||||
| 	color: #fc0; | ||||
| } | ||||
| html.light input[type="radio"]:checked+label { | ||||
| 	color: #07c; | ||||
| } | ||||
| input.eq_gain { | ||||
| 	width: 3em; | ||||
| 	text-align: center; | ||||
| 	margin: 0 .6em; | ||||
| } | ||||
| #audio_eq table { | ||||
| 	border-collapse: collapse; | ||||
| } | ||||
| #audio_eq td { | ||||
| 	text-align: center; | ||||
| } | ||||
| #audio_eq a.eq_step { | ||||
| 	font-size: 1.5em; | ||||
| 	display: block; | ||||
| 	padding: 0; | ||||
| } | ||||
| #au_eq { | ||||
| 	display: block; | ||||
| 	margin-top: .5em; | ||||
| 	padding: 1.3em .3em; | ||||
| } | ||||
|  | ||||
|  | ||||
|  | ||||
| @@ -563,6 +644,7 @@ input[type="checkbox"]:checked+label { | ||||
| } | ||||
| #wrap { | ||||
| 	margin-top: 2em; | ||||
| 	min-height: 90vh; | ||||
| } | ||||
| #tree { | ||||
| 	display: none; | ||||
| @@ -575,8 +657,15 @@ input[type="checkbox"]:checked+label { | ||||
| 	overscroll-behavior-y: none; | ||||
| 	scrollbar-color: #eb0 #333; | ||||
| } | ||||
| #treeh { | ||||
| 	background: #333; | ||||
| 	position: sticky; | ||||
| 	z-index: 1; | ||||
| 	top: 0; | ||||
| } | ||||
| #thx_ff { | ||||
| 	padding: 5em 0; | ||||
| 	/* widget */ | ||||
| } | ||||
| #tree::-webkit-scrollbar-track, | ||||
| #tree::-webkit-scrollbar { | ||||
| @@ -600,6 +689,7 @@ input[type="checkbox"]:checked+label { | ||||
| 	box-shadow: 0 .1em .2em #222 inset; | ||||
| 	border-radius: .3em; | ||||
| 	margin: .2em; | ||||
| 	white-space: pre; | ||||
| 	position: relative; | ||||
| 	top: -.2em; | ||||
| } | ||||
| @@ -636,15 +726,14 @@ input[type="checkbox"]:checked+label { | ||||
| #treeul a.hl { | ||||
| 	color: #400; | ||||
| 	background: #fc4; | ||||
| 	border-radius: .3em; | ||||
| 	text-shadow: none; | ||||
| } | ||||
| #treeul a { | ||||
| 	border-radius: .3em; | ||||
| 	display: inline-block; | ||||
| } | ||||
| #treeul a+a { | ||||
| 	width: calc(100% - 2em); | ||||
| 	background: #333; | ||||
| 	line-height: 1em; | ||||
| } | ||||
| #treeul a+a:hover { | ||||
| @@ -668,34 +757,20 @@ input[type="checkbox"]:checked+label { | ||||
| 	font-size: 2em; | ||||
| 	white-space: nowrap; | ||||
| } | ||||
| #files th:hover .cfg, | ||||
| #files th.min .cfg { | ||||
| #files th:hover .cfg { | ||||
| 	display: block; | ||||
| 	width: 1em; | ||||
| 	border-radius: .2em; | ||||
| 	margin: -1.3em auto 0 auto; | ||||
| 	background: #444; | ||||
| } | ||||
| #files th.min .cfg { | ||||
| 	margin: -.6em; | ||||
| } | ||||
| #files>thead>tr>th.min span { | ||||
| 	position: absolute; | ||||
| 	transform: rotate(270deg); | ||||
| 	background: linear-gradient(90deg, rgba(68,68,68,0), rgba(68,68,68,0.5) 70%, #444); | ||||
| 	margin-left: -4.6em; | ||||
| 	padding: .4em; | ||||
| 	top: 5.4em; | ||||
| 	width: 8em; | ||||
| 	text-align: right; | ||||
| 	letter-spacing: .04em; | ||||
| #files>thead>tr>th.min, | ||||
| #files td.min { | ||||
| 		display: none; | ||||
| } | ||||
| #files td:nth-child(2n) { | ||||
| 	color: #f5a; | ||||
| } | ||||
| #files td.min a { | ||||
| 	display: none; | ||||
| } | ||||
| #files tr.play td, | ||||
| #files tr.play div a { | ||||
| 	background: #fc4; | ||||
| @@ -710,49 +785,37 @@ input[type="checkbox"]:checked+label { | ||||
| 	color: #300; | ||||
| 	background: #fea; | ||||
| } | ||||
| #op_cfg { | ||||
| .opwide { | ||||
| 	max-width: none; | ||||
| 	margin-right: 1.5em; | ||||
| } | ||||
| #op_cfg>div>a { | ||||
| .opwide>div { | ||||
| 	display: inline-block; | ||||
| 	vertical-align: top; | ||||
| 	border-left: .2em solid #4c4c4c; | ||||
| 	margin-left: .5em; | ||||
| 	padding-left: .5em; | ||||
| } | ||||
| .opwide>div.fill { | ||||
| 	display: block; | ||||
| } | ||||
| .opwide>div>div>a { | ||||
| 	line-height: 2em; | ||||
| } | ||||
| #op_cfg>div>span { | ||||
| #op_cfg>div>div>span { | ||||
| 	display: inline-block; | ||||
| 	padding: .2em .4em; | ||||
| } | ||||
| #op_cfg h3 { | ||||
| .opbox h3 { | ||||
| 	margin: .8em 0 0 .6em; | ||||
| 	padding: 0; | ||||
| 	border-bottom: 1px solid #555; | ||||
| } | ||||
| #opdesc { | ||||
| 	display: none; | ||||
| } | ||||
| #ops:hover #opdesc { | ||||
| 	display: block; | ||||
| 	background: linear-gradient(0deg,#555, #4c4c4c 80%, #444); | ||||
| 	box-shadow: 0 .3em 1em #222; | ||||
| 	padding: 1em; | ||||
| 	border-radius: .3em; | ||||
| 	position: absolute; | ||||
| 	z-index: 3; | ||||
| 	top: 6em; | ||||
| 	right: 1.5em; | ||||
| } | ||||
| #ops:hover #opdesc.off { | ||||
| 	display: none; | ||||
| } | ||||
| #opdesc code { | ||||
| 	background: #3c3c3c; | ||||
| 	padding: .2em .3em; | ||||
| 	border-top: 1px solid #777; | ||||
| 	border-radius: .3em; | ||||
| 	font-family: monospace, monospace; | ||||
| 	line-height: 2em; | ||||
| #thumbs { | ||||
| 	opacity: .3; | ||||
| } | ||||
| #griden.on+#thumbs { | ||||
| 	opacity: .3; | ||||
| 	opacity: 1; | ||||
| } | ||||
| #ghead { | ||||
| 	background: #3c3c3c; | ||||
| @@ -798,6 +861,12 @@ html.light #ghead { | ||||
| 	padding: .2em .3em; | ||||
| 	display: block; | ||||
| } | ||||
| #ggrid span.dir:before { | ||||
| 	content: '📂'; | ||||
| 	line-height: 0; | ||||
| 	font-size: 2em; | ||||
| 	margin: -.7em .1em -.5em -.3em; | ||||
| } | ||||
| #ggrid a:hover { | ||||
| 	background: #444; | ||||
| 	border-color: #555; | ||||
| @@ -848,6 +917,15 @@ html.light { | ||||
| 	background: #eee; | ||||
| 	text-shadow: none; | ||||
| } | ||||
| html.light #tt { | ||||
| 	background: #fff; | ||||
| 	border-color: #888; | ||||
| 	box-shadow: 0 .3em 1em rgba(0,0,0,0.4); | ||||
| } | ||||
| html.light #tt code { | ||||
| 	background: #060; | ||||
| 	color: #fff; | ||||
| } | ||||
| html.light #ops, | ||||
| html.light .opbox, | ||||
| html.light #srch_form { | ||||
| @@ -891,8 +969,14 @@ html.light #treeul a.hl { | ||||
| 	background: #07a; | ||||
| 	color: #fff; | ||||
| } | ||||
| html.light #treeul a.hl:hover { | ||||
| 	background: #059; | ||||
| } | ||||
| html.light #tree li { | ||||
| 	border-color: #ddd #fff #f7f7f7 #fff; | ||||
| 	border-color: #f7f7f7 #fff #ddd #fff; | ||||
| } | ||||
| html.light #tree a:hover { | ||||
| 	background: #fff; | ||||
| } | ||||
| html.light #tree ul { | ||||
| 	border-color: #ccc; | ||||
| @@ -910,12 +994,14 @@ html.light #files { | ||||
| } | ||||
| html.light #files thead th { | ||||
| 	background: #eee; | ||||
| 	border: 1px solid #ccc; | ||||
| 	border-top: none; | ||||
| } | ||||
| html.light #files tr td { | ||||
| 	border-top: 1px solid #ddd; | ||||
| html.light #files thead th+th { | ||||
| 	border-left: 1px solid #f7f7f7; | ||||
| } | ||||
| html.light #files td { | ||||
| 	border-bottom: 1px solid #f7f7f7; | ||||
| 	border-color: #fff #fff #ddd #ddd; | ||||
| } | ||||
| html.light #files tbody tr:last-child td { | ||||
| 	border-bottom: .2em solid #ccc; | ||||
| @@ -923,25 +1009,28 @@ html.light #files tbody tr:last-child td { | ||||
| html.light #files td:nth-child(2n) { | ||||
| 	color: #d38; | ||||
| } | ||||
| html.light #files tr:hover td { | ||||
| 	background: #fff; | ||||
| html.light #files tr.play td:nth-child(2n) { | ||||
| 	color: #c16; | ||||
| } | ||||
| html.light #files tbody a.play { | ||||
| 	color: #c0f; | ||||
| } | ||||
| html.light tr.play td { | ||||
| html.light #files tbody a.play.act { | ||||
| 	color: #90c; | ||||
| } | ||||
| html.light #files tr.play td { | ||||
| 	background: #fc5; | ||||
| 	border-color: #eb1; | ||||
| } | ||||
| html.light #files tr:hover td { | ||||
| 	background: #fff; | ||||
| } | ||||
| html.light tr.play a { | ||||
| 	color: #406; | ||||
| } | ||||
| html.light #files th:hover .cfg, | ||||
| html.light #files th.min .cfg { | ||||
| html.light #files th:hover .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; | ||||
| } | ||||
| @@ -951,7 +1040,24 @@ html.light #blk_abrt a { | ||||
| 	box-shadow: 0 .2em .4em #ddd; | ||||
| } | ||||
| html.light #widget a { | ||||
| 	color: #fc5; | ||||
| 	color: #06a; | ||||
| } | ||||
| html.light #wtoggle, | ||||
| html.light #widgeti { | ||||
| 	background: #eee; | ||||
| } | ||||
| html.light #wtoggle { | ||||
| 	box-shadow: 0 0 .5em #bbb; | ||||
| } | ||||
| html.light #widget.open { | ||||
| 	border-top: .2em solid #f7f7f7; | ||||
| } | ||||
| html.light #wzip, | ||||
| html.light #wnp { | ||||
| 	border-color: #ccc; | ||||
| } | ||||
| html.light #barbuf { | ||||
| 	background: none; | ||||
| } | ||||
| html.light #files tr.sel:hover td { | ||||
| 	background: #c37; | ||||
| @@ -968,20 +1074,15 @@ html.light #files tr.sel a.play.act { | ||||
| html.light input[type="checkbox"] + label { | ||||
| 	color: #333; | ||||
| } | ||||
| html.light .opwide>div { | ||||
| 	border-color: #ccc; | ||||
| } | ||||
| 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; | ||||
| @@ -991,9 +1092,6 @@ html.light #path { | ||||
| 	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; | ||||
| @@ -1002,7 +1100,7 @@ html.light #path a:not(:last-child)::after { | ||||
| } | ||||
| html.light #path a:hover { | ||||
| 	background: none; | ||||
| 	color: #60a; | ||||
| 	color: #90d; | ||||
| } | ||||
| html.light #files tbody div a { | ||||
| 	color: #d38; | ||||
| @@ -1012,6 +1110,9 @@ html.light #files tr.sel a:hover { | ||||
| 	color: #000; | ||||
| 	background: #fff; | ||||
| } | ||||
| html.light #treeh { | ||||
| 	background: #eee; | ||||
| } | ||||
| html.light #tree { | ||||
| 	scrollbar-color: #a70 #ddd; | ||||
| } | ||||
| @@ -1021,4 +1122,162 @@ html.light #tree::-webkit-scrollbar { | ||||
| } | ||||
| #tree::-webkit-scrollbar-thumb { | ||||
| 	background: #da0; | ||||
| } | ||||
| } | ||||
|  | ||||
|  | ||||
|  | ||||
| #baguetteBox-overlay { | ||||
| 	display: none; | ||||
| 	opacity: 0; | ||||
| 	position: fixed; | ||||
| 	overflow: hidden; | ||||
| 	touch-action: none; | ||||
| 	top: 0; | ||||
| 	left: 0; | ||||
| 	width: 100%; | ||||
| 	height: 100%; | ||||
| 	z-index: 1000000; | ||||
| 	background: rgba(0, 0, 0, 0.8); | ||||
| 	transition: opacity .3s ease; | ||||
| } | ||||
| #baguetteBox-overlay.visible { | ||||
| 	opacity: 1; | ||||
| } | ||||
| #baguetteBox-overlay .full-image { | ||||
|     display: inline-block; | ||||
|     position: relative; | ||||
|     width: 100%; | ||||
|     height: 100%; | ||||
|     text-align: center; | ||||
| } | ||||
| #baguetteBox-overlay .full-image figure { | ||||
| 	display: inline; | ||||
| 	margin: 0; | ||||
| 	height: 100%; | ||||
| } | ||||
| #baguetteBox-overlay .full-image img { | ||||
| 	display: inline-block; | ||||
| 	width: auto; | ||||
| 	height: auto; | ||||
| 	max-height: 100%; | ||||
| 	max-width: 100%; | ||||
| 	vertical-align: middle; | ||||
| 	box-shadow: 0 0 8px rgba(0, 0, 0, 0.6); | ||||
| } | ||||
| #baguetteBox-overlay .full-image figcaption { | ||||
| 	display: block; | ||||
| 	position: absolute; | ||||
| 	bottom: 0; | ||||
| 	width: 100%; | ||||
| 	text-align: center; | ||||
| 	line-height: 1.8; | ||||
| 	white-space: normal; | ||||
| 	color: #ccc; | ||||
| } | ||||
| #baguetteBox-overlay figcaption a { | ||||
| 	background: rgba(0, 0, 0, 0.6); | ||||
| 	border-radius: .4em; | ||||
| 	padding: .3em .6em; | ||||
| } | ||||
| #baguetteBox-overlay .full-image:before { | ||||
| 	content: ""; | ||||
| 	display: inline-block; | ||||
| 	height: 50%; | ||||
| 	width: 1px; | ||||
| 	margin-right: -1px; | ||||
| } | ||||
| #baguetteBox-slider { | ||||
| 	position: absolute; | ||||
| 	left: 0; | ||||
| 	top: 0; | ||||
| 	height: 100%; | ||||
| 	width: 100%; | ||||
| 	white-space: nowrap; | ||||
| 	transition: left .2s ease, transform .2s ease; | ||||
| } | ||||
| #baguetteBox-slider.bounce-from-right { | ||||
| 	animation: bounceFromRight .4s ease-out; | ||||
| } | ||||
| #baguetteBox-slider.bounce-from-left { | ||||
| 	animation: bounceFromLeft .4s ease-out; | ||||
| } | ||||
| @keyframes bounceFromRight { | ||||
| 	0% {margin-left: 0} | ||||
| 	50% {margin-left: -30px} | ||||
| 	100% {margin-left: 0} | ||||
| } | ||||
| @keyframes bounceFromLeft { | ||||
| 	0% {margin-left: 0} | ||||
| 	50% {margin-left: 30px} | ||||
| 	100% {margin-left: 0} | ||||
| } | ||||
| .baguetteBox-button#next-button, | ||||
| .baguetteBox-button#previous-button { | ||||
| 	top: 50%; | ||||
| 	top: calc(50% - 30px); | ||||
| 	width: 44px; | ||||
| 	height: 60px; | ||||
| }  | ||||
| .baguetteBox-button { | ||||
| 	position: absolute; | ||||
| 	cursor: pointer; | ||||
| 	outline: none; | ||||
| 	padding: 0; | ||||
| 	margin: 0; | ||||
| 	border: 0; | ||||
| 	border-radius: 15%; | ||||
| 	background: rgba(50, 50, 50, 0.5); | ||||
| 	color: #ddd; | ||||
| 	font: 1.6em sans-serif; | ||||
| 	transition: background-color .3s ease; | ||||
| } | ||||
| .baguetteBox-button:focus, | ||||
| .baguetteBox-button:hover { | ||||
| 	background: rgba(50, 50, 50, 0.9); | ||||
| } | ||||
| #next-button { | ||||
| 	right: 2%; | ||||
| } | ||||
| #previous-button { | ||||
| 	left: 2%; | ||||
| } | ||||
| #close-button { | ||||
| 	top: 20px; | ||||
| 	right: 2%; | ||||
| 	width: 30px; | ||||
| 	height: 30px; | ||||
| } | ||||
| .baguetteBox-button svg { | ||||
| 	position: absolute; | ||||
| 	left: 0; | ||||
| 	top: 0; | ||||
| } | ||||
| .baguetteBox-spinner { | ||||
| 	width: 40px; | ||||
| 	height: 40px; | ||||
| 	display: inline-block; | ||||
| 	position: absolute; | ||||
| 	top: 50%; | ||||
| 	left: 50%; | ||||
| 	margin-top: -20px; | ||||
| 	margin-left: -20px; | ||||
| } | ||||
| .baguetteBox-double-bounce1, | ||||
| .baguetteBox-double-bounce2 { | ||||
| 	width: 100%; | ||||
| 	height: 100%; | ||||
| 	border-radius: 50%; | ||||
| 	background-color: #fff; | ||||
| 	opacity: .6; | ||||
| 	position: absolute; | ||||
| 	top: 0; | ||||
| 	left: 0; | ||||
| 	animation: bounce 2s infinite ease-in-out; | ||||
| } | ||||
| .baguetteBox-double-bounce2 { | ||||
| 	animation-delay: -1s; | ||||
| } | ||||
| @keyframes bounce { | ||||
| 	0%, 100% {transform: scale(0)} | ||||
| 	50% {transform: scale(1)} | ||||
| } | ||||
|   | ||||
| @@ -2,131 +2,134 @@ | ||||
| <html lang="en"> | ||||
|  | ||||
| <head> | ||||
|     <meta charset="utf-8"> | ||||
|     <title>⇆🎉 {{ title }}</title> | ||||
|     <meta http-equiv="X-UA-Compatible" content="IE=edge"> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=0.8"> | ||||
|     <link rel="stylesheet" type="text/css" media="screen" href="/.cpr/browser.css{{ ts }}"> | ||||
|     <link rel="stylesheet" type="text/css" media="screen" href="/.cpr/upload.css{{ ts }}"> | ||||
| 	<meta charset="utf-8"> | ||||
| 	<title>⇆🎉 {{ title }}</title> | ||||
| 	<meta http-equiv="X-UA-Compatible" content="IE=edge"> | ||||
| 	<meta name="viewport" content="width=device-width, initial-scale=0.8"> | ||||
| 	<link rel="stylesheet" type="text/css" media="screen" href="/.cpr/browser.css{{ ts }}"> | ||||
| 	<link rel="stylesheet" type="text/css" media="screen" href="/.cpr/upload.css{{ ts }}"> | ||||
| 	{%- if css %} | ||||
| 	<link rel="stylesheet" type="text/css" media="screen" href="{{ css }}{{ ts }}"> | ||||
| 	{%- endif %} | ||||
| </head> | ||||
|  | ||||
| <body> | ||||
|     <div id="ops"> | ||||
|         <a href="#" data-dest="" data-desc="close submenu">---</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> | ||||
|         {%- endif %} | ||||
|         <a href="#" data-perm="write" data-dest="bup" data-desc="bup: basic uploader, even supports netscape 4.0">🎈</a> | ||||
|         <a href="#" data-perm="write" data-dest="mkdir" data-desc="mkdir: create a new directory">📂</a> | ||||
|         <a href="#" data-perm="read write" data-dest="new_md" data-desc="new-md: create a new markdown document">📝</a> | ||||
|         <a href="#" data-perm="write" data-dest="msg" data-desc="msg: send a message to the server log">📟</a> | ||||
|         <a href="#" data-dest="cfg" data-desc="configuration options">⚙️</a> | ||||
|         <div id="opdesc"></div> | ||||
|     </div> | ||||
| 	<div id="ops"></div> | ||||
|  | ||||
|     <div id="op_search" class="opview"> | ||||
|         {%- if have_tags_idx %} | ||||
|         <div id="srch_form" class="tags"></div> | ||||
|         {%- else %} | ||||
|         <div id="srch_form"></div> | ||||
|         {%- endif %} | ||||
|         <div id="srch_q"></div> | ||||
|     </div> | ||||
| 	<div id="op_search" class="opview"> | ||||
| 		{%- if have_tags_idx %} | ||||
| 		<div id="srch_form" class="tags"></div> | ||||
| 		{%- else %} | ||||
| 		<div id="srch_form"></div> | ||||
| 		{%- endif %} | ||||
| 		<div id="srch_q"></div> | ||||
| 	</div> | ||||
|  | ||||
|     {%- include 'upload.html' %} | ||||
| 	<div id="op_player" class="opview opbox opwide"></div> | ||||
|  | ||||
|     <div id="op_cfg" class="opview opbox"> | ||||
|         <h3>switches</h3> | ||||
|         <div> | ||||
|             <a id="tooltips" class="tgl btn" href="#">tooltips</a> | ||||
|             <a id="lightmode" class="tgl btn" href="#">lightmode</a> | ||||
|             <a id="griden" class="tgl btn" href="#">the grid</a> | ||||
|             <a id="thumbs" class="tgl btn" href="#">thumbs</a> | ||||
|         </div> | ||||
|         {%- if have_zip %} | ||||
|         <h3>folder download</h3> | ||||
|         <div id="arc_fmt"></div> | ||||
|         {%- endif %} | ||||
|         <h3>key notation</h3> | ||||
|         <div id="key_notation"></div> | ||||
|     </div> | ||||
|      | ||||
|     <h1 id="path"> | ||||
|         <a href="#" id="entree">🌲</a> | ||||
|         {%- for n in vpnodes %} | ||||
|         <a href="/{{ n[0] }}">{{ n[1] }}</a> | ||||
|         {%- endfor %} | ||||
|     </h1> | ||||
|      | ||||
|     <div id="tree"> | ||||
|         <a href="#" id="detree">🍞...</a> | ||||
|         <a href="#" class="btn" step="2" id="twobytwo">+</a> | ||||
|         <a href="#" class="btn" step="-2" id="twig">–</a> | ||||
|         <a href="#" class="tgl btn" id="dyntree">a</a> | ||||
|         <ul id="treeul"></ul> | ||||
|         <div id="thx_ff"> </div> | ||||
|     </div> | ||||
| 	<div id="op_bup" class="opview opbox act"> | ||||
| 		<div id="u2err"></div> | ||||
| 		<form method="post" enctype="multipart/form-data" accept-charset="utf-8" action="{{ url_suf }}"> | ||||
| 			<input type="hidden" name="act" value="bput" /> | ||||
| 			<input type="file" name="f" multiple><br /> | ||||
| 			<input type="submit" value="start upload"> | ||||
| 		</form> | ||||
| 	</div> | ||||
|  | ||||
| 	<div id="op_mkdir" class="opview opbox act"> | ||||
| 		<form method="post" enctype="multipart/form-data" accept-charset="utf-8" action="{{ url_suf }}"> | ||||
| 			<input type="hidden" name="act" value="mkdir" /> | ||||
| 			<input type="text" name="name" size="30"> | ||||
| 			<input type="submit" value="mkdir"> | ||||
| 		</form> | ||||
| 	</div> | ||||
|  | ||||
| 	<div id="op_new_md" class="opview opbox"> | ||||
| 		<form method="post" enctype="multipart/form-data" accept-charset="utf-8" action="{{ url_suf }}"> | ||||
| 			<input type="hidden" name="act" value="new_md" /> | ||||
| 			<input type="text" name="name" size="30"> | ||||
| 			<input type="submit" value="create doc"> | ||||
| 		</form> | ||||
| 	</div> | ||||
|  | ||||
| 	<div id="op_msg" class="opview opbox act"> | ||||
| 		<form method="post" enctype="application/x-www-form-urlencoded" accept-charset="utf-8" action="{{ url_suf }}"> | ||||
| 			<input type="text" name="msg" size="30"> | ||||
| 			<input type="submit" value="send msg"> | ||||
| 		</form> | ||||
| 	</div> | ||||
|  | ||||
| 	<div id="op_up2k" class="opview"></div> | ||||
|  | ||||
| 	<div id="op_cfg" class="opview opbox opwide"></div> | ||||
| 	 | ||||
| 	<h1 id="path"> | ||||
| 		<a href="#" id="entree" tt="show directory tree$NHotkey: B">🌲</a> | ||||
| 		{%- for n in vpnodes %} | ||||
| 		<a href="/{{ n[0] }}">{{ n[1] }}</a> | ||||
| 		{%- endfor %} | ||||
| 	</h1> | ||||
| 	 | ||||
| 	<div id="tree"></div> | ||||
|  | ||||
| <div id="wrap"> | ||||
|  | ||||
|     <div id="pro" class="logue">{{ logues[0] }}</div> | ||||
| 	<div id="pro" class="logue">{{ logues[0] }}</div> | ||||
|  | ||||
|     <table id="files"> | ||||
|         <thead> | ||||
|             <tr> | ||||
|                 <th name="lead"><span>c</span></th> | ||||
|                 <th name="href"><span>File Name</span></th> | ||||
|                 <th name="sz" sort="int"><span>Size</span></th> | ||||
|                 {%- for k in taglist %} | ||||
|                     {%- if k.startswith('.') %} | ||||
|                         <th name="tags/{{ k }}" sort="int"><span>{{ k[1:] }}</span></th> | ||||
|                     {%- else %} | ||||
|                         <th name="tags/{{ k }}"><span>{{ k[0]|upper }}{{ k[1:] }}</span></th> | ||||
|                     {%- endif %} | ||||
|                 {%- endfor %} | ||||
|                 <th name="ext"><span>T</span></th> | ||||
|                 <th name="ts"><span>Date</span></th> | ||||
|             </tr> | ||||
|         </thead> | ||||
|         <tbody> | ||||
| 	<table id="files"> | ||||
| 		<thead> | ||||
| 			<tr> | ||||
| 				<th name="lead"><span>c</span></th> | ||||
| 				<th name="href"><span>File Name</span></th> | ||||
| 				<th name="sz" sort="int"><span>Size</span></th> | ||||
| 				{%- for k in taglist %} | ||||
| 					{%- if k.startswith('.') %} | ||||
| 				<th name="tags/{{ k }}" sort="int"><span>{{ k[1:] }}</span></th> | ||||
| 					{%- else %} | ||||
| 				<th name="tags/{{ k }}"><span>{{ k[0]|upper }}{{ k[1:] }}</span></th> | ||||
| 					{%- endif %} | ||||
| 				{%- endfor %} | ||||
| 				<th name="ext"><span>T</span></th> | ||||
| 				<th name="ts"><span>Date</span></th> | ||||
| 			</tr> | ||||
| 		</thead> | ||||
| <tbody> | ||||
|  | ||||
| {%- for f in files %} | ||||
|     <tr><td>{{ f.lead }}</td><td><a href="{{ f.href }}">{{ f.name|e }}</a></td><td>{{ f.sz }}</td> | ||||
|     {%- if f.tags is defined %} | ||||
|         {%- for k in taglist %} | ||||
|             <td>{{ f.tags[k] }}</td> | ||||
|         {%- endfor %} | ||||
|     {%- endif %} | ||||
|     <td>{{ f.ext }}</td><td>{{ f.dt }}</td></tr> | ||||
| <tr><td>{{ f.lead }}</td><td><a href="{{ f.href }}">{{ f.name|e }}</a></td><td>{{ f.sz }}</td> | ||||
| 	{%- if f.tags is defined %} | ||||
| 		{%- for k in taglist %} | ||||
| <td>{{ f.tags[k] }}</td> | ||||
| 		{%- endfor %} | ||||
| 	{%- endif %} | ||||
| <td>{{ f.ext }}</td><td>{{ f.dt }}</td></tr> | ||||
| {%- endfor %} | ||||
|  | ||||
|         </tbody> | ||||
|     </table> | ||||
|      | ||||
|     <div id="epi" class="logue">{{ logues[1] }}</div> | ||||
| 		</tbody> | ||||
| 	</table> | ||||
| 	 | ||||
| 	<div id="epi" class="logue">{{ logues[1] }}</div> | ||||
|  | ||||
|     <h2><a href="?h">control-panel</a></h2> | ||||
| 	<h2><a href="?h">control-panel</a></h2> | ||||
|  | ||||
| </div> | ||||
|  | ||||
|     {%- if srv_info %} | ||||
|     <div id="srv_info"><span>{{ srv_info }}</span></div> | ||||
|     {%- endif %} | ||||
| 	{%- if srv_info %} | ||||
| 	<div id="srv_info"><span>{{ srv_info }}</span></div> | ||||
| 	{%- endif %} | ||||
|  | ||||
|     <div id="widget"></div> | ||||
| 	<div id="widget"></div> | ||||
|  | ||||
|     <script> | ||||
|         var tag_order_cfg = {{ tag_order }}; | ||||
|     </script> | ||||
|     <script src="/.cpr/util.js{{ ts }}"></script> | ||||
|     <script src="/.cpr/browser.js{{ ts }}"></script> | ||||
|     <script src="/.cpr/up2k.js{{ ts }}"></script> | ||||
|     <script> | ||||
|         apply_perms({{ perms }}); | ||||
|     </script> | ||||
| 	<script> | ||||
| 		var perms = {{ perms }}, | ||||
| 			tag_order_cfg = {{ tag_order }}, | ||||
| 			have_up2k_idx = {{ have_up2k_idx|tojson }}, | ||||
| 			have_tags_idx = {{ have_tags_idx|tojson }}, | ||||
| 			have_zip = {{ have_zip|tojson }}; | ||||
| 	</script> | ||||
| 	<script src="/.cpr/util.js{{ ts }}"></script> | ||||
| 	<script src="/.cpr/browser.js{{ ts }}"></script> | ||||
| 	<script src="/.cpr/up2k.js{{ ts }}"></script> | ||||
| </body> | ||||
|  | ||||
| </html> | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -2,59 +2,59 @@ | ||||
| <html lang="en"> | ||||
|  | ||||
| <head> | ||||
|     <meta charset="utf-8"> | ||||
|     <title>{{ title }}</title> | ||||
|     <meta http-equiv="X-UA-Compatible" content="IE=edge"> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=0.8"> | ||||
|     <style> | ||||
|         html{font-family:sans-serif} | ||||
|         td{border:1px solid #999;border-width:1px 1px 0 0;padding:0 5px} | ||||
|         a{display:block} | ||||
|     </style> | ||||
| 	<meta charset="utf-8"> | ||||
| 	<title>{{ title }}</title> | ||||
| 	<meta http-equiv="X-UA-Compatible" content="IE=edge"> | ||||
| 	<meta name="viewport" content="width=device-width, initial-scale=0.8"> | ||||
| 	<style> | ||||
| 		html{font-family:sans-serif} | ||||
| 		td{border:1px solid #999;border-width:1px 1px 0 0;padding:0 5px} | ||||
| 		a{display:block} | ||||
| 	</style> | ||||
| </head> | ||||
|  | ||||
| <body> | ||||
|     {%- if srv_info %} | ||||
|     <p><span>{{ srv_info }}</span></p> | ||||
|     {%- endif %} | ||||
| 	{%- if srv_info %} | ||||
| 	<p><span>{{ srv_info }}</span></p> | ||||
| 	{%- endif %} | ||||
|  | ||||
|     {%- if have_b_u %} | ||||
|     <form method="post" enctype="multipart/form-data" accept-charset="utf-8" action="{{ url_suf }}"> | ||||
|         <input type="hidden" name="act" value="bput" /> | ||||
|         <input type="file" name="f" multiple /><br /> | ||||
|         <input type="submit" value="start upload" /> | ||||
|     </form> | ||||
|     <br /> | ||||
|     {%- endif %} | ||||
| 	{%- if have_b_u %} | ||||
| 	<form method="post" enctype="multipart/form-data" accept-charset="utf-8" action="{{ url_suf }}"> | ||||
| 		<input type="hidden" name="act" value="bput" /> | ||||
| 		<input type="file" name="f" multiple /><br /> | ||||
| 		<input type="submit" value="start upload" /> | ||||
| 	</form> | ||||
| 	<br /> | ||||
| 	{%- endif %} | ||||
|  | ||||
|     {%- if logues[0] %} | ||||
|     <div>{{ logues[0] }}</div><br /> | ||||
|     {%- endif %} | ||||
| 	{%- if logues[0] %} | ||||
| 	<div>{{ logues[0] }}</div><br /> | ||||
| 	{%- endif %} | ||||
|  | ||||
|     <table id="files"> | ||||
|         <thead> | ||||
|             <tr> | ||||
|                 <th name="lead"><span>c</span></th> | ||||
|                 <th name="href"><span>File Name</span></th> | ||||
|                 <th name="sz" sort="int"><span>Size</span></th> | ||||
|                 <th name="ts"><span>Date</span></th> | ||||
|             </tr> | ||||
|         </thead> | ||||
|         <tbody> | ||||
|             <tr><td></td><td><a href="../{{ url_suf }}">parent folder</a></td><td>-</td><td>-</td></tr> | ||||
| 	<table id="files"> | ||||
| 		<thead> | ||||
| 			<tr> | ||||
| 				<th name="lead"><span>c</span></th> | ||||
| 				<th name="href"><span>File Name</span></th> | ||||
| 				<th name="sz" sort="int"><span>Size</span></th> | ||||
| 				<th name="ts"><span>Date</span></th> | ||||
| 			</tr> | ||||
| 		</thead> | ||||
| 		<tbody> | ||||
| <tr><td></td><td><a href="../{{ url_suf }}">parent folder</a></td><td>-</td><td>-</td></tr> | ||||
|  | ||||
| {%- for f in files %} | ||||
|     <tr><td>{{ f.lead }}</td><td><a href="{{ f.href }}{{ url_suf }}">{{ f.name|e }}</a></td><td>{{ f.sz }}</td><td>{{ f.dt }}</td></tr> | ||||
| <tr><td>{{ f.lead }}</td><td><a href="{{ f.href }}{{ url_suf }}">{{ f.name|e }}</a></td><td>{{ f.sz }}</td><td>{{ f.dt }}</td></tr> | ||||
| {%- endfor %} | ||||
|  | ||||
|         </tbody> | ||||
|     </table> | ||||
|      | ||||
|     {%- if logues[1] %} | ||||
|     <div>{{ logues[1] }}</div><br /> | ||||
|     {%- endif %} | ||||
|      | ||||
|     <h2><a href="{{ url_suf }}{{ url_suf and '&' or '?' }}h">control-panel</a></h2> | ||||
| 		</tbody> | ||||
| 	</table> | ||||
| 	 | ||||
| 	{%- if logues[1] %} | ||||
| 	<div>{{ logues[1] }}</div><br /> | ||||
| 	{%- endif %} | ||||
| 	 | ||||
| 	<h2><a href="{{ url_suf }}{{ url_suf and '&' or '?' }}h">control-panel</a></h2> | ||||
|  | ||||
| </body> | ||||
| </html> | ||||
|   | ||||
							
								
								
									
										61
									
								
								copyparty/web/dbg-audio.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								copyparty/web/dbg-audio.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,61 @@ | ||||
| var ofun = audio_eq.apply.bind(audio_eq); | ||||
| audio_eq.apply = function () { | ||||
|     var ac1 = mp.ac; | ||||
|     ofun(); | ||||
|     var ac = mp.ac, | ||||
|         w = 2048, | ||||
|         h = 256; | ||||
|  | ||||
|     if (!audio_eq.filters.length) { | ||||
|         audio_eq.ana = null; | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     var can = ebi('fft_can'); | ||||
|     if (!can) { | ||||
|         can = mknod('canvas'); | ||||
|         can.setAttribute('id', 'fft_can'); | ||||
|         can.style.cssText = 'position:absolute;left:0;bottom:5em;width:' + w + 'px;height:' + h + 'px;z-index:9001'; | ||||
|         document.body.appendChild(can); | ||||
|         can.width = w; | ||||
|         can.height = h; | ||||
|     } | ||||
|     var cc = can.getContext('2d'); | ||||
|     if (!ac) | ||||
|         return; | ||||
|  | ||||
|     var ana = ac.createAnalyser(); | ||||
|     ana.smoothingTimeConstant = 0; | ||||
|     ana.fftSize = 8192; | ||||
|  | ||||
|     audio_eq.filters[0].connect(ana); | ||||
|     audio_eq.ana = ana; | ||||
|  | ||||
|     var buf = new Uint8Array(ana.frequencyBinCount), | ||||
|         colw = can.width / buf.length; | ||||
|  | ||||
|     cc.fillStyle = '#fc0'; | ||||
|     function draw() { | ||||
|         if (ana == audio_eq.ana) | ||||
|             requestAnimationFrame(draw); | ||||
|  | ||||
|         ana.getByteFrequencyData(buf); | ||||
|  | ||||
|         cc.clearRect(0, 0, can.width, can.height); | ||||
|  | ||||
|         /*var x = 0, w = 1; | ||||
|         for (var a = 0; a < buf.length; a++) { | ||||
|             cc.fillRect(x, h - buf[a], w, h); | ||||
|             x += w; | ||||
|         }*/ | ||||
|         var mul = Math.pow(w, 4) / buf.length; | ||||
|         for (var x = 0; x < w; x++) { | ||||
|             var a = Math.floor(Math.pow(x, 4) / mul), | ||||
|                 v = buf[a]; | ||||
|  | ||||
|             cc.fillRect(x, h - v, 1, v); | ||||
|         } | ||||
|     } | ||||
|     draw(); | ||||
| }; | ||||
| audio_eq.apply(); | ||||
| @@ -26,10 +26,23 @@ a { | ||||
| 	border-radius: .2em; | ||||
| 	padding: .2em .8em; | ||||
| } | ||||
| td, th { | ||||
| table { | ||||
| 	border-collapse: collapse; | ||||
| } | ||||
| .vols td, | ||||
| .vols th { | ||||
| 	padding: .3em .6em; | ||||
| 	text-align: left; | ||||
| } | ||||
| .num { | ||||
| 	border-right: 1px solid #bbb; | ||||
| } | ||||
| .num td { | ||||
| 	padding: .1em .7em .1em 0; | ||||
| } | ||||
| .num td:first-child { | ||||
| 	text-align: right; | ||||
| } | ||||
| .btns { | ||||
| 	margin: 1em 0; | ||||
| } | ||||
| @@ -57,4 +70,7 @@ html.dark input { | ||||
| 	border-radius: .5em; | ||||
| 	padding: .5em .7em; | ||||
| 	margin: 0 .5em 0 0; | ||||
| } | ||||
| html.dark .num { | ||||
| 	border-color: #777; | ||||
| } | ||||
| @@ -15,16 +15,25 @@ | ||||
|  | ||||
|         {%- if avol %} | ||||
|         <h1>admin panel:</h1> | ||||
|         <table> | ||||
|             <thead><tr><th>vol</th><th>action</th><th>status</th></tr></thead> | ||||
|             <tbody> | ||||
|                 {% for mp in avol %} | ||||
|                 {%- if mp in vstate and vstate[mp] %} | ||||
|                 <tr><td><a href="{{ mp }}{{ url_suf }}">{{ mp }}</a></td><td><a href="{{ mp }}?scan">rescan</a></td><td>{{ vstate[mp] }}</td></tr> | ||||
|                 {%- endif %} | ||||
|                 {% endfor %} | ||||
|             </tbody> | ||||
|         </table> | ||||
|         <table><tr><td> <!-- hehehe --> | ||||
|             <table class="num"> | ||||
|                 <tr><td>scanning</td><td>{{ scanning }}</td></tr> | ||||
|                 <tr><td>hash-q</td><td>{{ hashq }}</td></tr> | ||||
|                 <tr><td>tag-q</td><td>{{ tagq }}</td></tr> | ||||
|                 <tr><td>mtp-q</td><td>{{ mtpq }}</td></tr> | ||||
|             </table> | ||||
|         </td><td> | ||||
|             <table class="vols"> | ||||
|                 <thead><tr><th>vol</th><th>action</th><th>status</th></tr></thead> | ||||
|                 <tbody> | ||||
|                     {% for mp in avol %} | ||||
|                     {%- if mp in vstate and vstate[mp] %} | ||||
|                     <tr><td><a href="{{ mp }}{{ url_suf }}">{{ mp }}</a></td><td><a href="{{ mp }}?scan">rescan</a></td><td>{{ vstate[mp] }}</td></tr> | ||||
|                     {%- endif %} | ||||
|                     {% endfor %} | ||||
|                 </tbody> | ||||
|             </table> | ||||
|         </td></tr></table> | ||||
|         <div class="btns"> | ||||
|             <a href="{{ avol[0] }}?stack">dump stack</a> | ||||
|         </div> | ||||
| @@ -50,7 +59,7 @@ | ||||
|  | ||||
|         <h1>login for more:</h1> | ||||
|         <ul> | ||||
|             <form method="post" enctype="multipart/form-data" action="/{{ url_suf }}"> | ||||
|             <form method="post" enctype="multipart/form-data" action="/"> | ||||
|                 <input type="hidden" name="act" value="login" /> | ||||
|                 <input type="password" name="cppwd" /> | ||||
|                 <input type="submit" value="Login" /> | ||||
|   | ||||
| @@ -444,8 +444,7 @@ function up2k_init(subtle) { | ||||
|     } | ||||
|  | ||||
|     // show uploader if the user only has write-access | ||||
|     var perms = document.body.getAttribute('perms'); | ||||
|     if (perms && !has(perms.split(' '), 'read')) | ||||
|     if (perms.length && !has(perms, 'read')) | ||||
|         goto('up2k'); | ||||
|  | ||||
|     // shows or clears a message in the basic uploader ui | ||||
| @@ -804,6 +803,14 @@ function up2k_init(subtle) { | ||||
|  | ||||
|                 var mou_ikkai = false; | ||||
|  | ||||
|                 if (st.busy.handshake.length > 0 && | ||||
|                     st.busy.handshake[0].busied < Date.now() - 30 * 1000 | ||||
|                 ) { | ||||
|                     console.log("retrying stuck handshake"); | ||||
|                     var t = st.busy.handshake.shift(); | ||||
|                     st.todo.handshake.unshift(t); | ||||
|                 } | ||||
|  | ||||
|                 if (st.todo.handshake.length > 0 && | ||||
|                     st.busy.handshake.length == 0 && ( | ||||
|                         st.todo.handshake[0].t4 || ( | ||||
| @@ -963,8 +970,8 @@ function up2k_init(subtle) { | ||||
|             while (segm_next()); | ||||
|  | ||||
|             var hash_done = function (hashbuf) { | ||||
|                 var hslice = new Uint8Array(hashbuf).subarray(0, 32), | ||||
|                     b64str = buf2b64(hslice).replace(/=$/, ''); | ||||
|                 var hslice = new Uint8Array(hashbuf).subarray(0, 33), | ||||
|                     b64str = buf2b64(hslice); | ||||
|  | ||||
|                 hashtab[nch] = b64str; | ||||
|                 t.hash.push(nch); | ||||
| @@ -989,6 +996,7 @@ function up2k_init(subtle) { | ||||
|                 pvis.seth(t.n, 1, '📦 wait'); | ||||
|                 st.busy.hash.splice(st.busy.hash.indexOf(t), 1); | ||||
|                 st.todo.handshake.push(t); | ||||
|                 tasker(); | ||||
|             }; | ||||
|  | ||||
|             if (subtle) | ||||
| @@ -1019,11 +1027,28 @@ function up2k_init(subtle) { | ||||
|     // | ||||
|  | ||||
|     function exec_handshake() { | ||||
|         var t = st.todo.handshake.shift(); | ||||
|         var t = st.todo.handshake.shift(), | ||||
|             me = Date.now(); | ||||
|  | ||||
|         st.busy.handshake.push(t); | ||||
|         t.busied = me; | ||||
|  | ||||
|         var xhr = new XMLHttpRequest(); | ||||
|         xhr.onerror = function () { | ||||
|             if (t.busied != me) { | ||||
|                 console.log('zombie handshake onerror,', t); | ||||
|                 return; | ||||
|             } | ||||
|             console.log('handshake onerror, retrying'); | ||||
|             st.busy.handshake.splice(st.busy.handshake.indexOf(t), 1); | ||||
|             st.todo.handshake.unshift(t); | ||||
|             tasker(); | ||||
|         }; | ||||
|         xhr.onload = function (e) { | ||||
|             if (t.busied != me) { | ||||
|                 console.log('zombie handshake onload,', t); | ||||
|                 return; | ||||
|             } | ||||
|             if (xhr.status == 200) { | ||||
|                 var response = JSON.parse(xhr.responseText); | ||||
|  | ||||
| @@ -1241,7 +1266,7 @@ function up2k_init(subtle) { | ||||
|             fpx = parseInt(getComputedStyle(bar)['font-size']), | ||||
|             wem = wpx * 1.0 / fpx, | ||||
|             wide = wem > 54, | ||||
|             parent = ebi(wide ? 'u2btn_cw' : 'u2btn_ct'), | ||||
|             parent = ebi(wide && has(perms, 'write') ? 'u2btn_cw' : 'u2btn_ct'), | ||||
|             btn = ebi('u2btn'); | ||||
|  | ||||
|         //console.log([wpx, fpx, wem]); | ||||
| @@ -1254,31 +1279,18 @@ function up2k_init(subtle) { | ||||
|     window.addEventListener('resize', onresize); | ||||
|     onresize(); | ||||
|  | ||||
|     function desc_show(e) { | ||||
|         var cfg = sread('tooltips'); | ||||
|         if (cfg !== null && cfg != '1') | ||||
|             return; | ||||
|  | ||||
|         var msg = this.getAttribute('alt'), | ||||
|             cdesc = ebi('u2cdesc'); | ||||
|  | ||||
|         cdesc.innerHTML = msg.replace(/\$N/g, "<br />"); | ||||
|         cdesc.setAttribute('class', 'show'); | ||||
|     if (is_touch) { | ||||
|         // android-chrome wobbles for a bit; firefox / iOS-safari are OK | ||||
|         setTimeout(onresize, 20); | ||||
|         setTimeout(onresize, 100); | ||||
|         setTimeout(onresize, 500); | ||||
|     } | ||||
|     function desc_hide(e) { | ||||
|         ebi('u2cdesc').setAttribute('class', ''); | ||||
|     } | ||||
|     var o = QSA('#u2conf *[alt]'); | ||||
|  | ||||
|     var o = QSA('#u2conf *[tt]'); | ||||
|     for (var a = o.length - 1; a >= 0; a--) { | ||||
|         o[a].parentNode.getElementsByTagName('input')[0].setAttribute('alt', o[a].getAttribute('alt')); | ||||
|     } | ||||
|     var o = QSA('#u2conf *[alt]'); | ||||
|     for (var a = 0; a < o.length; a++) { | ||||
|         o[a].onfocus = desc_show; | ||||
|         o[a].onblur = desc_hide; | ||||
|         o[a].onmouseenter = desc_show; | ||||
|         o[a].onmouseleave = desc_hide; | ||||
|         o[a].parentNode.getElementsByTagName('input')[0].setAttribute('tt', o[a].getAttribute('tt')); | ||||
|     } | ||||
|     tt.init(); | ||||
|  | ||||
|     function bumpthread(dir) { | ||||
|         try { | ||||
| @@ -1326,14 +1338,12 @@ function up2k_init(subtle) { | ||||
|     } | ||||
|  | ||||
|     function set_fsearch(new_state) { | ||||
|         var perms = document.body.getAttribute('perms'), | ||||
|             fixed = false; | ||||
|         var fixed = false; | ||||
|  | ||||
|         if (!ebi('fsearch')) { | ||||
|             new_state = false; | ||||
|         } | ||||
|         else if (perms) { | ||||
|             perms = perms.split(' '); | ||||
|         else if (perms.length) { | ||||
|             if (!has(perms, 'write')) { | ||||
|                 new_state = true; | ||||
|                 fixed = true; | ||||
| @@ -1363,6 +1373,8 @@ function up2k_init(subtle) { | ||||
|             ebi('u2bm').innerHTML = ico + ' <sup>' + desc + '</sup>'; | ||||
|         } | ||||
|         catch (ex) { } | ||||
|  | ||||
|         onresize(); | ||||
|     } | ||||
|  | ||||
|     function tgl_flag_en() { | ||||
| @@ -1426,5 +1438,9 @@ function warn_uploader_busy(e) { | ||||
| } | ||||
|  | ||||
|  | ||||
| tt.init(); | ||||
|  | ||||
| if (QS('#op_up2k.act')) | ||||
|     goto_up2k(); | ||||
|  | ||||
| apply_perms(perms); | ||||
|   | ||||
| @@ -211,29 +211,6 @@ | ||||
| 	box-shadow: none; | ||||
| 	opacity: .2; | ||||
| } | ||||
| #u2cdesc { | ||||
| 	position: absolute; | ||||
| 	width: 34em; | ||||
| 	left: calc(50% - 15em); | ||||
| 	background: #222; | ||||
| 	border: 0 solid #555; | ||||
| 	text-align: center; | ||||
| 	overflow: hidden; | ||||
| 	margin: 0 -2em; | ||||
| 	padding: 0 1em; | ||||
| 	height: 0; | ||||
| 	opacity: .1; | ||||
| 	transition: all 0.14s ease-in-out; | ||||
| 	box-shadow: 0 .2em .5em #222; | ||||
| 	border-radius: .4em; | ||||
| 	z-index: 1; | ||||
| } | ||||
| #u2cdesc.show { | ||||
| 	padding: 1em; | ||||
| 	height: auto; | ||||
| 	border-width: .2em 0; | ||||
| 	opacity: 1; | ||||
| } | ||||
| #u2foot { | ||||
| 	color: #fff; | ||||
| 	font-style: italic; | ||||
| @@ -286,10 +263,6 @@ 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; | ||||
| } | ||||
|   | ||||
| @@ -1,103 +0,0 @@ | ||||
|  | ||||
|     <div id="op_bup" class="opview opbox act"> | ||||
|         <div id="u2err"></div> | ||||
|         <form method="post" enctype="multipart/form-data" accept-charset="utf-8" action="{{ url_suf }}"> | ||||
|             <input type="hidden" name="act" value="bput" /> | ||||
|             <input type="file" name="f" multiple><br /> | ||||
|             <input type="submit" value="start upload"> | ||||
|         </form> | ||||
|     </div> | ||||
|  | ||||
|     <div id="op_mkdir" class="opview opbox act"> | ||||
|         <form method="post" enctype="multipart/form-data" accept-charset="utf-8" action="{{ url_suf }}"> | ||||
|             <input type="hidden" name="act" value="mkdir" /> | ||||
|             <input type="text" name="name" size="30"> | ||||
|             <input type="submit" value="mkdir"> | ||||
|         </form> | ||||
|     </div> | ||||
|  | ||||
|     <div id="op_new_md" class="opview opbox"> | ||||
|         <form method="post" enctype="multipart/form-data" accept-charset="utf-8" action="{{ url_suf }}"> | ||||
|             <input type="hidden" name="act" value="new_md" /> | ||||
|             <input type="text" name="name" size="30"> | ||||
|             <input type="submit" value="create doc"> | ||||
|         </form> | ||||
|     </div> | ||||
|  | ||||
|     <div id="op_msg" class="opview opbox act"> | ||||
|         <form method="post" enctype="application/x-www-form-urlencoded" accept-charset="utf-8" action="{{ url_suf }}"> | ||||
|             <input type="text" name="msg" size="30"> | ||||
|             <input type="submit" value="send msg"> | ||||
|         </form> | ||||
|     </div> | ||||
|  | ||||
|     <div id="op_up2k" class="opview"> | ||||
|         <form id="u2form" method="post" enctype="multipart/form-data" onsubmit="return false;"></form> | ||||
|  | ||||
|             <table id="u2conf"> | ||||
|                 <tr> | ||||
|                     <td><br />parallel uploads:</td> | ||||
|                     <td rowspan="2"> | ||||
|                         <input type="checkbox" id="multitask" /> | ||||
|                         <label for="multitask" alt="continue hashing other files while uploading">🏃</label> | ||||
|                     </td> | ||||
|                     <td rowspan="2"> | ||||
|                         <input type="checkbox" id="ask_up" /> | ||||
|                         <label for="ask_up" alt="ask for confirmation befofre upload starts">💭</label> | ||||
|                     </td> | ||||
|                     <td rowspan="2"> | ||||
|                         <input type="checkbox" id="flag_en" /> | ||||
|                         <label for="flag_en" alt="ensure only one tab is uploading at a time $N (other tabs must have this enabled too)">💤</label> | ||||
|                     </td> | ||||
|                 {%- if have_up2k_idx %} | ||||
|                     <td data-perm="read" rowspan="2"> | ||||
|                         <input type="checkbox" id="fsearch" /> | ||||
|                         <label for="fsearch" alt="don't actually upload, instead check if the files already $N exist on the server (will scan all folders you can read)">🔎</label> | ||||
|                     </td> | ||||
|                 {%- endif %} | ||||
|                     <td data-perm="read" rowspan="2" id="u2btn_cw"></td> | ||||
|                 </tr> | ||||
|                 <tr> | ||||
|                     <td> | ||||
|                         <a href="#" id="nthread_sub">–</a><input | ||||
|                             class="txtbox" id="nthread" value="2"/><a | ||||
|                             href="#" id="nthread_add">+</a><br />  | ||||
|                     </td> | ||||
|                 </tr> | ||||
|             </table> | ||||
|  | ||||
|             <div id="u2cdesc"></div> | ||||
|  | ||||
|             <div id="u2notbtn"></div> | ||||
|  | ||||
|             <div id="u2btn_ct"> | ||||
|                 <div id="u2btn"> | ||||
|                     <span id="u2bm"></span><br /> | ||||
|                     drag/drop files<br /> | ||||
|                     and folders here<br /> | ||||
|                     (or click me) | ||||
|                 </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"> | ||||
|                 <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> | ||||
|             <p id="u2footfoot" data-perm="write">( you can use the <a href="#" id="u2nope">basic uploader</a> if you don't need lastmod timestamps, resumable uploads, or progress bars )</p> | ||||
|     </div> | ||||
| @@ -6,7 +6,7 @@ if (!window['console']) | ||||
|     }; | ||||
|  | ||||
|  | ||||
| var clickev = window.Touch ? 'touchstart' : 'click', | ||||
| var is_touch = 'ontouchstart' in window, | ||||
|     ANDROID = /(android)/i.test(navigator.userAgent); | ||||
|  | ||||
|  | ||||
| @@ -285,63 +285,6 @@ function makeSortable(table, cb) { | ||||
| } | ||||
|  | ||||
|  | ||||
| (function () { | ||||
|     var ops = QSA('#ops>a'); | ||||
|     for (var a = 0; a < ops.length; a++) { | ||||
|         ops[a].onclick = opclick; | ||||
|     } | ||||
| })(); | ||||
|  | ||||
|  | ||||
| function opclick(e) { | ||||
|     ev(e); | ||||
|  | ||||
|     var dest = this.getAttribute('data-dest'); | ||||
|     goto(dest); | ||||
|  | ||||
|     swrite('opmode', dest || null); | ||||
|  | ||||
|     var input = QS('.opview.act input:not([type="hidden"])') | ||||
|     if (input) | ||||
|         input.focus(); | ||||
| } | ||||
|  | ||||
|  | ||||
| function goto(dest) { | ||||
|     var obj = QSA('.opview.act'); | ||||
|     for (var a = obj.length - 1; a >= 0; a--) | ||||
|         clmod(obj[a], 'act'); | ||||
|  | ||||
|     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); | ||||
|         QS('#ops>a[data-dest=' + dest + ']').className += " act"; | ||||
|  | ||||
|         var fn = window['goto_' + dest]; | ||||
|         if (fn) | ||||
|             fn(); | ||||
|     } | ||||
|  | ||||
|     if (window['treectl']) | ||||
|         treectl.onscroll(); | ||||
| } | ||||
|  | ||||
|  | ||||
| (function () { | ||||
|     goto(); | ||||
|     var op = sread('opmode'); | ||||
|     if (op !== null && op !== '.') | ||||
|         try { | ||||
|             goto(op); | ||||
|         } | ||||
|         catch (ex) { } | ||||
| })(); | ||||
|  | ||||
|  | ||||
| function linksplit(rp) { | ||||
|     var ret = []; | ||||
|     var apath = '/'; | ||||
| @@ -528,3 +471,67 @@ function hist_replace(url) { | ||||
|     console.log("h-repl " + url); | ||||
|     history.replaceState(url, url, url); | ||||
| } | ||||
|  | ||||
|  | ||||
| var tt = (function () { | ||||
|     var r = { | ||||
|         "tt": mknod("div"), | ||||
|         "en": true | ||||
|     }; | ||||
|  | ||||
|     r.tt.setAttribute('id', 'tt'); | ||||
|     document.body.appendChild(r.tt); | ||||
|  | ||||
|     function show() { | ||||
|         var cfg = sread('tooltips'); | ||||
|         if (cfg !== null && cfg != '1') | ||||
|             return; | ||||
|  | ||||
|         var msg = this.getAttribute('tt'); | ||||
|         if (!msg) | ||||
|             return; | ||||
|  | ||||
|         var pos = this.getBoundingClientRect(), | ||||
|             left = pos.left < window.innerWidth / 2, | ||||
|             top = pos.top < window.innerHeight / 2; | ||||
|  | ||||
|         r.tt.style.top = top ? pos.bottom + 'px' : 'auto'; | ||||
|         r.tt.style.bottom = top ? 'auto' : (window.innerHeight - pos.top) + 'px'; | ||||
|         r.tt.style.left = left ? pos.left + 'px' : 'auto'; | ||||
|         r.tt.style.right = left ? 'auto' : (window.innerWidth - pos.right) + 'px'; | ||||
|  | ||||
|         r.tt.innerHTML = msg.replace(/\$N/g, "<br />"); | ||||
|         clmod(r.tt, 'show', 1); | ||||
|     } | ||||
|  | ||||
|     function hide() { | ||||
|         clmod(r.tt, 'show'); | ||||
|     } | ||||
|  | ||||
|     r.init = function () { | ||||
|         var ttb = ebi('tooltips'); | ||||
|         if (ttb) { | ||||
|             ttb.onclick = function (e) { | ||||
|                 ev(e); | ||||
|                 r.en = !r.en; | ||||
|                 bcfg_set('tooltips', r.en); | ||||
|                 r.init(); | ||||
|             }; | ||||
|             r.en = bcfg_get('tooltips', true) | ||||
|         } | ||||
|  | ||||
|         var _show = r.en ? show : null, | ||||
|             _hide = r.en ? hide : null; | ||||
|  | ||||
|         var o = QSA('*[tt]'); | ||||
|         for (var a = o.length - 1; a >= 0; a--) { | ||||
|             o[a].onfocus = _show; | ||||
|             o[a].onblur = _hide; | ||||
|             o[a].onmouseenter = _show; | ||||
|             o[a].onmouseleave = _hide; | ||||
|         } | ||||
|         hide(); | ||||
|     }; | ||||
|  | ||||
|     return r; | ||||
| })(); | ||||
|   | ||||
							
								
								
									
										22
									
								
								docs/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								docs/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| # example `.epilogue.html` | ||||
| save one of these as `.epilogue.html` inside a folder to customize it: | ||||
|  | ||||
| * [`minimal-up2k.html`](minimal-up2k.html) will [simplify the upload ui](https://user-images.githubusercontent.com/241032/118311195-dd6ca380-b4ef-11eb-86f3-75a3ff2e1332.png) | ||||
|  | ||||
|  | ||||
|  | ||||
| # example browser-css | ||||
| point `--css-browser` to one of these by URL: | ||||
|  | ||||
| * [`browser.css`](browser.css) changes the background | ||||
| * [`browser-icons.css`](browser-icons.css) adds filetype icons | ||||
|  | ||||
|  | ||||
|  | ||||
| # other stuff | ||||
|  | ||||
| ## [`rclone.md`](rclone.md) | ||||
| * notes on using rclone as a fuse client/server | ||||
|  | ||||
| ## [`example.conf`](example.conf) | ||||
| * example config file for `-c` which never really happened | ||||
							
								
								
									
										95
									
								
								docs/biquad.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								docs/biquad.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,95 @@ | ||||
| <!DOCTYPE html><html><head></head><body><script> | ||||
|  | ||||
| setTimeout(location.reload.bind(location), 700); | ||||
| document.documentElement.scrollLeft = 0; | ||||
|  | ||||
| var can = document.createElement('canvas'), | ||||
|     cc = can.getContext('2d'), | ||||
|     w = 2048, | ||||
|     h = 1024; | ||||
|  | ||||
| w = 2048; | ||||
|  | ||||
| can.width = w; | ||||
| can.height = h; | ||||
| document.body.appendChild(can); | ||||
| can.style.cssText = 'width:' + w + 'px;height:' + h + 'px'; | ||||
|  | ||||
| cc.fillStyle = '#000'; | ||||
| cc.fillRect(0, 0, w, h); | ||||
|  | ||||
| var cfg = [ // hz, q, g | ||||
|     [31.25 * 0.88, 0, 1.4],  // shelf | ||||
|     [31.25 * 1.04, 0.7, 0.96],  // peak | ||||
|     [62.5, 0.7, 1], | ||||
|     [125, 0.8, 1], | ||||
|     [250, 0.9, 1.03], | ||||
|     [500, 0.9, 1.1], | ||||
|     [1000, 0.9, 1.1], | ||||
|     [2000, 0.9, 1.105], | ||||
|     [4000, 0.88, 1.05], | ||||
|     [8000 * 1.006, 0.73, 1.24], | ||||
|     //[16000 * 1.00, 0.5, 1.75],  // peak.v1 | ||||
|     //[16000 * 1.19, 0, 1.8]  // shelf.v1 | ||||
|     [16000 * 0.89, 0.7, 1.26],  // peak | ||||
|     [16000 * 1.13, 0.82, 1.09],  // peak | ||||
|     [16000 * 1.205, 0, 1.9]  // shelf | ||||
| ]; | ||||
|  | ||||
| var freqs = new Float32Array(22000), | ||||
|     sum = new Float32Array(freqs.length), | ||||
|     ac = new AudioContext(), | ||||
|     step = w / freqs.length, | ||||
|     colors = [ | ||||
|         'rgba(255, 0, 0, 0.7)', | ||||
|         'rgba(0, 224, 0, 0.7)', | ||||
|         'rgba(0, 64, 255, 0.7)' | ||||
|     ]; | ||||
|  | ||||
| var order = []; | ||||
|  | ||||
| for (var a = 0; a < cfg.length; a += 2) | ||||
|     order.push(a); | ||||
|  | ||||
| for (var a = 1; a < cfg.length; a += 2) | ||||
|     order.push(a); | ||||
|  | ||||
| for (var ia = 0; ia < order.length; ia++) { | ||||
|     var a = order[ia], | ||||
|         fi = ac.createBiquadFilter(), | ||||
|         mag = new Float32Array(freqs.length), | ||||
|         phase = new Float32Array(freqs.length); | ||||
|  | ||||
|     for (var b = 0; b < freqs.length; b++) | ||||
|         freqs[b] = b; | ||||
|  | ||||
|     fi.type = a == 0 ? 'lowshelf' : a == cfg.length - 1 ? 'highshelf' : 'peaking'; | ||||
|     fi.frequency.value = cfg[a][0]; | ||||
|     fi.Q.value = cfg[a][1]; | ||||
|     fi.gain.value = 1; | ||||
|  | ||||
|     fi.getFrequencyResponse(freqs, mag, phase); | ||||
|     cc.fillStyle = colors[a % colors.length]; | ||||
|     for (var b = 0; b < sum.length; b++) { | ||||
|         mag[b] -= 1; | ||||
|         sum[b] += mag[b] * cfg[a][2]; | ||||
|         var y = h - (mag[b] * h * 3); | ||||
|         cc.fillRect(b * step, y, step, h - y); | ||||
|         cc.fillRect(b * step - 1, y - 1, 3, 3); | ||||
|     } | ||||
| } | ||||
|  | ||||
| var min = 999999, max = 0; | ||||
| for (var a = 0; a < sum.length; a++) { | ||||
|     min = Math.min(min, sum[a]); | ||||
|     max = Math.max(max, sum[a]); | ||||
| } | ||||
| cc.fillStyle = 'rgba(255,255,255,1)'; | ||||
| for (var a = 0; a < sum.length; a++) { | ||||
|     var v = (sum[a] - min) / (max - min); | ||||
|     cc.fillRect(a * step, 0, step, v * h / 2); | ||||
| } | ||||
|  | ||||
| cc.fillRect(0, 460, w, 1); | ||||
|  | ||||
| </script></body></html> | ||||
							
								
								
									
										68
									
								
								docs/browser-icons.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								docs/browser-icons.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,68 @@ | ||||
| /* put filetype icons inline with text | ||||
| #ggrid>a>span:before, | ||||
| #ggrid>a>span.dir:before { | ||||
| 	display: inline; | ||||
| 	line-height: 0; | ||||
| 	font-size: 1.7em; | ||||
| 	margin: -.7em .1em -.5em -.6em; | ||||
| } | ||||
| */ | ||||
|  | ||||
|  | ||||
| /* move folder icons top-left */ | ||||
| #ggrid>a>span.dir:before { | ||||
| 	content: initial; | ||||
| } | ||||
| #ggrid>a[href$="/"]:before { | ||||
| 	content: '📂'; | ||||
|     display: block; | ||||
|     position: absolute; | ||||
|     margin: -.1em -.4em; | ||||
|     text-shadow: 0 0 .1em #000; | ||||
|     font-size: 2em; | ||||
| } | ||||
|  | ||||
|  | ||||
| /* put filetype icons top-left */ | ||||
| #ggrid>a:before { | ||||
|     display: block; | ||||
|     position: absolute; | ||||
|     margin: -.1em -.4em; | ||||
|     text-shadow: 0 0 .1em #000; | ||||
|     font-size: 2em; | ||||
| } | ||||
|  | ||||
|  | ||||
| /* video */ | ||||
| #ggrid>a:is( | ||||
| [href$=".mkv"i], | ||||
| [href$=".mp4"i], | ||||
| [href$=".webm"i], | ||||
| ):before { | ||||
|     content: '📺'; | ||||
| } | ||||
|  | ||||
|  | ||||
| /* audio */ | ||||
| #ggrid>a:is( | ||||
| [href$=".mp3"i], | ||||
| [href$=".ogg"i], | ||||
| [href$=".opus"i], | ||||
| [href$=".flac"i], | ||||
| [href$=".m4a"i], | ||||
| [href$=".aac"i], | ||||
| ):before { | ||||
|     content: '🎵'; | ||||
| } | ||||
|  | ||||
|  | ||||
| /* image */ | ||||
| #ggrid>a:is( | ||||
| [href$=".jpg"i], | ||||
| [href$=".jpeg"i], | ||||
| [href$=".png"i], | ||||
| [href$=".gif"i], | ||||
| [href$=".webp"i], | ||||
| ):before { | ||||
|     content: '🎨'; | ||||
| } | ||||
							
								
								
									
										29
									
								
								docs/browser.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								docs/browser.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| html { | ||||
|     background: #333 url('/wp/wallhaven-mdjrqy.jpg') center / cover no-repeat fixed; | ||||
| } | ||||
| #files th { | ||||
|     background: rgba(32, 32, 32, 0.9) !important; | ||||
| } | ||||
| #ops, | ||||
| #treeul, | ||||
| #files td { | ||||
|     background: rgba(32, 32, 32, 0.3) !important; | ||||
| } | ||||
|  | ||||
|  | ||||
| html.light { | ||||
|     background: #eee url('/wp/wallhaven-dpxl6l.png') center / cover no-repeat fixed; | ||||
| } | ||||
| html.light #files th { | ||||
|     background: rgba(255, 255, 255, 0.9) !important; | ||||
| } | ||||
| html.light #ops, | ||||
| html.light #treeul, | ||||
| html.light #files td { | ||||
|     background: rgba(248, 248, 248, 0.8) !important; | ||||
| } | ||||
|  | ||||
|  | ||||
| #files * { | ||||
|     background: transparent !important; | ||||
| } | ||||
| @@ -86,6 +86,9 @@ var t=[]; var b=document.location.href.split('#')[0].slice(0, -1); document.quer | ||||
| # get the size and video-id of all youtube vids in folder, assuming filename ends with -id.ext, and create a copyparty search query | ||||
| find -maxdepth 1 -printf '%s %p\n' | sort -n | awk '!/-([0-9a-zA-Z_-]{11})\.(mkv|mp4|webm)$/{next} {sub(/\.[^\.]+$/,"");n=length($0);v=substr($0,n-10);print $1, v}' | tee /dev/stderr | awk 'BEGIN {p="("} {printf("%s name like *-%s.* ",p,$2);p="or"} END {print ")\n"}' | cat >&2 | ||||
|  | ||||
| # unique stacks in a stackdump | ||||
| f=a; rm -rf stacks; mkdir stacks; grep -E '^#' $f | while IFS= read -r n; do awk -v n="$n" '!$0{o=0} o; $0==n{o=1}' <$f >stacks/f; h=$(sha1sum <stacks/f | cut -c-16); mv stacks/f stacks/$h-"$n"; done ; find stacks/ | sort | uniq -cw24 | ||||
|  | ||||
|  | ||||
| ## | ||||
| ## sqlite3 stuff | ||||
| @@ -153,6 +156,9 @@ dbg.asyncStore.pendingBreakpoints = {} | ||||
| # fix firefox phantom breakpoints | ||||
| about:config >> devtools.debugger.prefs-schema-version = -1 | ||||
|  | ||||
| # determine server version | ||||
| git reset --hard origin/HEAD && git log --format=format:"%H %ai %d" --decorate=full > /dev/shm/revs && cat /dev/shm/revs | while read -r rev extra; do (git reset --hard $rev >/dev/null 2>/dev/null && dsz=$(cat copyparty/web/{util,browser,up2k}.js 2>/dev/null | diff -wNarU0 - <(cat /mnt/Users/ed/Downloads/ref/{util,browser,up2k}.js) | wc -c) && printf '%s %6s %s\n' "$rev" $dsz "$extra") </dev/null; done                 | ||||
|  | ||||
|  | ||||
| ## | ||||
| ## http 206 | ||||
|   | ||||
							
								
								
									
										32
									
								
								docs/tcp-debug.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								docs/tcp-debug.sh
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| (cd ~/dev/copyparty && strace -Tttyyvfs 256 -o strace.strace python3 -um copyparty -i 127.0.0.1 --http-only --stackmon /dev/shm/cpps,10 ) 2>&1 | tee /dev/stderr > ~/log-copyparty-$(date +%Y-%m%d-%H%M%S).txt | ||||
|  | ||||
| 14/Jun/2021:16:34:02 1623688447.212405 death | ||||
| 14/Jun/2021:16:35:02 1623688502.420860 back | ||||
|  | ||||
| tcpdump -nni lo -w /home/ed/lo.pcap | ||||
|  | ||||
| # 16:35:25.324662 IP 127.0.0.1.48632 > 127.0.0.1.3920: Flags [F.], seq 849, ack 544, win 359, options [nop,nop,TS val 809396796 ecr 809396796], length 0 | ||||
|  | ||||
| tcpdump -nnr /home/ed/lo.pcap | awk '/ > 127.0.0.1.3920: /{sub(/ > .*/,"");sub(/.*\./,"");print}' | sort -n | uniq | while IFS= read -r port; do echo; tcpdump -nnr /home/ed/lo.pcap 2>/dev/null | grep -E "\.$port( > |: F)" | sed -r 's/ > .*, /, /'; done | grep -E '^16:35:0.*length [^0]' -C50 | ||||
|  | ||||
| 16:34:02.441732 IP 127.0.0.1.48638, length 0 | ||||
| 16:34:02.441738 IP 127.0.0.1.3920, length 0 | ||||
| 16:34:02.441744 IP 127.0.0.1.48638, length 0 | ||||
| 16:34:02.441756 IP 127.0.0.1.48638, length 791 | ||||
| 16:34:02.441759 IP 127.0.0.1.3920, length 0 | ||||
| 16:35:02.445529 IP 127.0.0.1.48638, length 0 | ||||
| 16:35:02.489194 IP 127.0.0.1.3920, length 0 | ||||
| 16:35:02.515595 IP 127.0.0.1.3920, length 216 | ||||
| 16:35:02.515600 IP 127.0.0.1.48638, length 0 | ||||
|  | ||||
| grep 48638 "$(find ~ -maxdepth 1 -name log-copyparty-\*.txt | sort | tail -n 1)" | ||||
|  | ||||
| 1623688502.510380 48638 rh | ||||
| 1623688502.511291 48638 Unrecv direct ... | ||||
| 1623688502.511827 48638 rh = 791 | ||||
| 16:35:02.518 127.0.0.1 48638       shut(8): [Errno 107] Socket not connected | ||||
| Exception in thread httpsrv-0.1-48638: | ||||
|  | ||||
| grep 48638 ~/dev/copyparty/strace.strace | ||||
| 14561 16:35:02.506310 <... accept4 resumed> {sa_family=AF_INET, sin_port=htons(48638), sin_addr=inet_addr("127.0.0.1")}, [16], SOCK_CLOEXEC) = 8<TCP:[127.0.0.1:3920->127.0.0.1:48638]> <0.000012> | ||||
| 15230 16:35:02.510725 write(1<pipe:[256639555]>, "1623688502.510380 48638 rh\n", 27 <unfinished ...> | ||||
| @@ -92,20 +92,34 @@ chmod 755 \ | ||||
|   copyparty-extras/copyparty-*/{scripts,bin}/* | ||||
|  | ||||
|  | ||||
| # extract and repack the sfx with less features enabled | ||||
| # extract the sfx | ||||
| ( cd copyparty-extras/sfx-full/ | ||||
| ./copyparty-sfx.py -h | ||||
| cd ../copyparty-*/ | ||||
| ./scripts/make-sfx.sh re no-ogv no-cm | ||||
| ) | ||||
|  | ||||
|  | ||||
| # put new sfx into copyparty-extras/sfx-lite/, | ||||
| # fuse client into copyparty-extras/, | ||||
| repack() { | ||||
|  | ||||
| 	# do the repack | ||||
| 	(cd copyparty-extras/copyparty-*/ | ||||
| 	./scripts/make-sfx.sh $2 | ||||
| 	) | ||||
|  | ||||
| 	# put new sfx into copyparty-extras/$name/, | ||||
| 	( cd copyparty-extras/ | ||||
| 	mv copyparty-*/dist/* $1/ | ||||
| 	) | ||||
| } | ||||
|  | ||||
| repack sfx-full "re gz no-sh" | ||||
| repack sfx-lite "re no-ogv no-cm" | ||||
| repack sfx-lite "re no-ogv no-cm gz no-sh" | ||||
|  | ||||
|  | ||||
| # move fuse client into copyparty-extras/, | ||||
| # copy lite-sfx.py to ./copyparty, | ||||
| # delete extracted source code | ||||
| ( cd copyparty-extras/ | ||||
| mv copyparty-*/dist/* sfx-lite/ | ||||
| mv copyparty-*/bin/copyparty-fuse.py . | ||||
| cp -pv sfx-lite/copyparty-sfx.py ../copyparty | ||||
| rm -rf copyparty-{0..9}*.*.*{0..9} | ||||
| @@ -119,6 +133,7 @@ true | ||||
|  | ||||
|  | ||||
| # create the bundle | ||||
| printf '\n\n' | ||||
| fn=copyparty-$(date +%Y-%m%d-%H%M%S).tgz | ||||
| tar -czvf "$od/$fn" * | ||||
| cd "$od" | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| FROM    alpine:3.13 | ||||
| WORKDIR /z | ||||
| ENV     ver_asmcrypto=5b994303a9d3e27e0915f72a10b6c2c51535a4dc \ | ||||
|         ver_hashwasm=4.7.0 \ | ||||
|         ver_marked=1.1.0 \ | ||||
|         ver_ogvjs=1.8.0 \ | ||||
|         ver_mde=2.14.0 \ | ||||
| @@ -9,12 +10,6 @@ ENV     ver_asmcrypto=5b994303a9d3e27e0915f72a10b6c2c51535a4dc \ | ||||
|         ver_zopfli=1.0.3 | ||||
|  | ||||
|  | ||||
| # TODO | ||||
| # sha512.hw.js https://github.com/Daninet/hash-wasm | ||||
| # sha512.kc.js https://github.com/chm-diederichs/sha3-wasm | ||||
| # awk '/HMAC state/{o=1}  /var HEAP/{o=0}  /function hmac_reset/{o=1}  /return \{/{o=0}  /var __extends =/{o=1}  /var Hash =/{o=0}  /hmac_|pbkdf2_/{next}  o{next}  {gsub(/IllegalStateError/,"Exception")}  {sub(/^ +/,"");sub(/^\/\/ .*/,"");sub(/;$/," ;")}  1' <sha512.ac.js.orig >sha512.ac.js; for fn in sha512.ac.js.orig sha512.ac.js; do wc -c <$fn; wc -c <$fn.gz ; for n in {1..9}; do printf '%8d %d bz\n' $(bzip2 -c$n <$fn | wc -c) $n; done; done | ||||
|  | ||||
|  | ||||
| # download; | ||||
| # the scp url is latin from https://fonts.googleapis.com/css2?family=Source+Code+Pro&display=swap | ||||
| RUN     mkdir -p /z/dist/no-pk \ | ||||
| @@ -27,7 +22,11 @@ RUN     mkdir -p /z/dist/no-pk \ | ||||
|         && wget https://github.com/codemirror/CodeMirror/archive/$ver_codemirror.tar.gz -O codemirror.tgz \ | ||||
|         && wget https://github.com/FortAwesome/Font-Awesome/releases/download/$ver_fontawesome/fontawesome-free-$ver_fontawesome-web.zip -O fontawesome.zip \ | ||||
|         && wget https://github.com/google/zopfli/archive/zopfli-$ver_zopfli.tar.gz -O zopfli.tgz \ | ||||
|         && wget https://github.com/Daninet/hash-wasm/releases/download/v$ver_hashwasm/hash-wasm@$ver_hashwasm.zip -O hash-wasm.zip \ | ||||
|         && unzip ogvjs.zip \ | ||||
|         && (mkdir hash-wasm \ | ||||
|             && cd hash-wasm \ | ||||
|             && unzip ../hash-wasm.zip) \ | ||||
|         && (tar -xf asmcrypto.tgz \ | ||||
|             && cd asmcrypto.js-$ver_asmcrypto \ | ||||
|             && npm install ) \ | ||||
| @@ -64,7 +63,12 @@ RUN     tar -xf zopfli.tgz \ | ||||
| RUN     cd asmcrypto.js-$ver_asmcrypto \ | ||||
|         && echo "export { Sha512 } from './hash/sha512/sha512';" > src/entry-export_all.ts \ | ||||
|         && node -r esm build.js \ | ||||
|         && mv asmcrypto.all.es5.js /z/dist/sha512.js | ||||
|         && awk '/HMAC state/{o=1}  /var HEAP/{o=0}  /function hmac_reset/{o=1}  /return \{/{o=0}  /var __extends =/{o=1}  /var Hash =/{o=0}  /hmac_|pbkdf2_/{next}  o{next}  {gsub(/IllegalStateError/,"Exception")}  {sub(/^ +/,"");sub(/^\/\/ .*/,"");sub(/;$/," ;")}  1' < asmcrypto.all.es5.js > /z/dist/sha512.ac.js | ||||
|  | ||||
|  | ||||
| # build hash-wasm | ||||
| RUN     cd hash-wasm \ | ||||
|         && mv sha512.umd.min.js /z/dist/sha512.hw.js | ||||
|  | ||||
|  | ||||
| # build ogvjs | ||||
|   | ||||
| @@ -11,6 +11,10 @@ echo | ||||
| # `re` does a repack of an sfx which you already executed once | ||||
| #   (grabs files from the sfx-created tempdir), overrides `clean` | ||||
| # | ||||
| # `gz` creates a gzip-compressed python sfx instead of bzip2 | ||||
| # | ||||
| # `no-sh` makes just the python sfx, skips the sh/unix sfx | ||||
| # | ||||
| # `no-ogv` saves ~500k by removing the opus/vorbis audio codecs | ||||
| #   (only affects apple devices; everything else has native support) | ||||
| # | ||||
| @@ -32,6 +36,10 @@ gtar=$(command -v gtar || command -v gnutar) || true | ||||
| 	[ -e /opt/local/bin/bzip2 ] && | ||||
| 		bzip2() { /opt/local/bin/bzip2 "$@"; } | ||||
| } | ||||
|  | ||||
| gawk=$(command -v gawk || command -v gnuawk || command -v awk) | ||||
| awk() { $gawk "$@"; } | ||||
|  | ||||
| pybin=$(command -v python3 || command -v python) || { | ||||
| 	echo need python | ||||
| 	exit 1 | ||||
| @@ -163,7 +171,7 @@ find .. -type f \( -name .DS_Store -or -name ._.DS_Store \) -delete | ||||
| find .. -type f -name ._\* | while IFS= read -r f; do cmp <(printf '\x00\x05\x16') <(head -c 3 -- "$f") && rm -f -- "$f"; done | ||||
|  | ||||
| echo use smol web deps | ||||
| rm -f copyparty/web/deps/*.full.* copyparty/web/Makefile | ||||
| rm -f copyparty/web/deps/*.full.* copyparty/web/dbg-* copyparty/web/Makefile | ||||
|  | ||||
| # it's fine dw | ||||
| grep -lE '\.full\.(js|css)' copyparty/web/* | | ||||
| @@ -194,11 +202,40 @@ tmv "$f" | ||||
|  | ||||
| # up2k goes from 28k to 22k laff | ||||
| echo entabbening | ||||
| find | grep -E '\.(js|css|html)$' | while IFS= read -r f; do | ||||
| find | grep -E '\.css$' | while IFS= read -r f; do | ||||
| 	awk '{ | ||||
| 		sub(/^[ \t]+/,""); | ||||
| 		sub(/[ \t]+$/,""); | ||||
| 		$0=gensub(/^([a-z-]+) *: *(.*[^ ]) *;$/,"\\1:\\2;","1"); | ||||
| 		sub(/ +\{$/,"{"); | ||||
| 		gsub(/, /,",") | ||||
| 	} | ||||
| 	!/\}$/ {printf "%s",$0;next} | ||||
| 	1 | ||||
| 	' <$f | sed 's/;\}$/}/' >t | ||||
| 	tmv "$f" | ||||
| done | ||||
| find | grep -E '\.(js|html)$' | while IFS= read -r f; do | ||||
| 	unexpand -t 4 --first-only <"$f" >t | ||||
| 	tmv "$f" | ||||
| done | ||||
|  | ||||
|  | ||||
| gzres() { | ||||
| command -v pigz && | ||||
| 	pk='pigz -11 -J 34 -I 100' || | ||||
| 	pk='gzip' | ||||
|  | ||||
| echo "$pk" | ||||
| find | grep -E '\.(js|css)$' | grep -vF /deps/ | while IFS= read -r f; do | ||||
| 	echo -n . | ||||
| 	$pk "$f" | ||||
| done | ||||
| echo | ||||
| } | ||||
| gzres | ||||
|  | ||||
|  | ||||
| echo gen tarlist | ||||
| for d in copyparty dep-j2; do find $d -type f; done | | ||||
| sed -r 's/(.*)\.(.*)/\2 \1/' | LC_ALL=C sort | | ||||
|   | ||||
| @@ -3,10 +3,13 @@ set -ex | ||||
|  | ||||
| pids=() | ||||
| for py in python{2,3}; do | ||||
|     $py -m unittest discover -s tests >/dev/null & | ||||
|     nice $py -m unittest discover -s tests >/dev/null & | ||||
|     pids+=($!) | ||||
| done | ||||
|  | ||||
| python3 scripts/test/smoketest.py & | ||||
| pids+=($!) | ||||
|  | ||||
| for pid in ${pids[@]}; do | ||||
|     wait $pid | ||||
| done | ||||
|   | ||||
| @@ -47,7 +47,7 @@ grep -E '/(python|pypy)[0-9\.-]*$' >$dir/pys || true | ||||
| 	printf '\033[1;30mlooking for jinja2 in [%s]\033[0m\n' "$_py" >&2 | ||||
| 	$_py -c 'import jinja2' 2>/dev/null || continue | ||||
| 	printf '%s\n' "$_py" | ||||
| 	mv $dir/{,x.}jinja2 | ||||
| 	mv $dir/{,x.}dep-j2 | ||||
| 	break | ||||
| done)" | ||||
|  | ||||
|   | ||||
							
								
								
									
										105
									
								
								scripts/test/race.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								scripts/test/race.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,105 @@ | ||||
| #!/usr/bin/env python3 | ||||
|  | ||||
| import os | ||||
| import sys | ||||
| import time | ||||
| import json | ||||
| import threading | ||||
| import http.client | ||||
|  | ||||
|  | ||||
| class Conn(object): | ||||
|     def __init__(self, ip, port): | ||||
|         self.s = http.client.HTTPConnection(ip, port, timeout=260) | ||||
|         self.st = [] | ||||
|  | ||||
|     def get(self, vpath): | ||||
|         self.st = [time.time()] | ||||
|  | ||||
|         self.s.request("GET", vpath) | ||||
|         self.st.append(time.time()) | ||||
|  | ||||
|         ret = self.s.getresponse() | ||||
|         self.st.append(time.time()) | ||||
|  | ||||
|         if ret.status < 200 or ret.status >= 400: | ||||
|             raise Exception(ret.status) | ||||
|  | ||||
|         ret = ret.read() | ||||
|         self.st.append(time.time()) | ||||
|  | ||||
|         return ret | ||||
|  | ||||
|     def get_json(self, vpath): | ||||
|         ret = self.get(vpath) | ||||
|         return json.loads(ret) | ||||
|  | ||||
|  | ||||
| class CState(threading.Thread): | ||||
|     def __init__(self, cs): | ||||
|         threading.Thread.__init__(self) | ||||
|         self.daemon = True | ||||
|         self.cs = cs | ||||
|         self.start() | ||||
|  | ||||
|     def run(self): | ||||
|         colors = [5, 1, 3, 2, 7] | ||||
|         remotes = [] | ||||
|         remotes_ok = False | ||||
|         while True: | ||||
|             time.sleep(0.001) | ||||
|             if not remotes_ok: | ||||
|                 remotes = [] | ||||
|                 remotes_ok = True | ||||
|                 for conn in self.cs: | ||||
|                     try: | ||||
|                         remotes.append(conn.s.sock.getsockname()[1]) | ||||
|                     except: | ||||
|                         remotes.append("?") | ||||
|                         remotes_ok = False | ||||
|  | ||||
|             m = [] | ||||
|             for conn, remote in zip(self.cs, remotes): | ||||
|                 stage = len(conn.st) | ||||
|                 m.append(f"\033[3{colors[stage]}m{remote}") | ||||
|  | ||||
|             m = " ".join(m) | ||||
|             print(f"{m}\033[0m\n\033[A", end="") | ||||
|  | ||||
|  | ||||
| def allget(cs, urls): | ||||
|     thrs = [] | ||||
|     for c, url in zip(cs, urls): | ||||
|         t = threading.Thread(target=c.get, args=(url,)) | ||||
|         t.start() | ||||
|         thrs.append(t) | ||||
|  | ||||
|     for t in thrs: | ||||
|         t.join() | ||||
|  | ||||
|  | ||||
| def main(): | ||||
|     os.system("") | ||||
|  | ||||
|     ip, port = sys.argv[1].split(":") | ||||
|     port = int(port) | ||||
|  | ||||
|     cs = [] | ||||
|     for _ in range(64): | ||||
|         cs.append(Conn(ip, 3923)) | ||||
|  | ||||
|     CState(cs) | ||||
|  | ||||
|     urlbase = "/doujin/c95" | ||||
|     j = cs[0].get_json(f"{urlbase}?ls") | ||||
|     urls = [] | ||||
|     for d in j["dirs"]: | ||||
|         urls.append(f"{urlbase}/{d['href']}?th=w") | ||||
|  | ||||
|     for n in range(100): | ||||
|         print(n) | ||||
|         allget(cs, urls) | ||||
|  | ||||
|  | ||||
| if __name__ == "__main__": | ||||
|     main() | ||||
							
								
								
									
										209
									
								
								scripts/test/smoketest.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										209
									
								
								scripts/test/smoketest.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,209 @@ | ||||
| import os | ||||
| import sys | ||||
| import time | ||||
| import shlex | ||||
| import shutil | ||||
| import signal | ||||
| import tempfile | ||||
| import requests | ||||
| import threading | ||||
| import subprocess as sp | ||||
|  | ||||
|  | ||||
| CPP = [] | ||||
|  | ||||
|  | ||||
| class Cpp(object): | ||||
|     def __init__(self, args): | ||||
|         args = [sys.executable, "-m", "copyparty"] + args | ||||
|         print(" ".join([shlex.quote(x) for x in args])) | ||||
|  | ||||
|         self.ls_pre = set(list(os.listdir())) | ||||
|         self.p = sp.Popen(args) | ||||
|         # , stdout=sp.PIPE, stderr=sp.PIPE) | ||||
|  | ||||
|         self.t = threading.Thread(target=self._run) | ||||
|         self.t.daemon = True | ||||
|         self.t.start() | ||||
|  | ||||
|     def _run(self): | ||||
|         self.so, self.se = self.p.communicate() | ||||
|  | ||||
|     def stop(self, wait): | ||||
|         if wait: | ||||
|             os.kill(self.p.pid, signal.SIGINT) | ||||
|             self.t.join(timeout=2) | ||||
|         else: | ||||
|             self.p.kill()  # macos py3.8 | ||||
|  | ||||
|     def clean(self): | ||||
|         t = os.listdir() | ||||
|         for f in t: | ||||
|             if f not in self.ls_pre and f.startswith("up."): | ||||
|                 os.unlink(f) | ||||
|  | ||||
|     def await_idle(self, ub, timeout): | ||||
|         req = ["scanning</td><td>False", "hash-q</td><td>0", "tag-q</td><td>0"] | ||||
|         lim = int(timeout * 10) | ||||
|         u = ub + "?h" | ||||
|         for n in range(lim): | ||||
|             try: | ||||
|                 time.sleep(0.1) | ||||
|                 r = requests.get(u, timeout=0.1) | ||||
|                 for x in req: | ||||
|                     if x not in r.text: | ||||
|                         print("ST: {}/{} miss {}".format(n, lim, x)) | ||||
|                         raise Exception() | ||||
|                 print("ST: idle") | ||||
|                 return | ||||
|             except: | ||||
|                 pass | ||||
|  | ||||
|  | ||||
| def tc1(): | ||||
|     ub = "http://127.0.0.1:4321/" | ||||
|     td = os.path.join("srv", "smoketest") | ||||
|     try: | ||||
|         shutil.rmtree(td) | ||||
|     except: | ||||
|         if os.path.exists(td): | ||||
|             raise | ||||
|  | ||||
|     for _ in range(10): | ||||
|         try: | ||||
|             os.mkdir(td) | ||||
|         except: | ||||
|             time.sleep(0.1)  # win10 | ||||
|  | ||||
|     assert os.path.exists(td) | ||||
|  | ||||
|     vidp = os.path.join(tempfile.gettempdir(), "smoketest.h264") | ||||
|     if not os.path.exists(vidp): | ||||
|         cmd = "ffmpeg -f lavfi -i testsrc=48x32:3 -t 1 -c:v libx264 -tune animation -preset veryslow -crf 69" | ||||
|         sp.check_call(cmd.split(" ") + [vidp]) | ||||
|  | ||||
|     with open(vidp, "rb") as f: | ||||
|         ovid = f.read() | ||||
|  | ||||
|     args = [ | ||||
|         "-p4321", | ||||
|         "-e2dsa", | ||||
|         "-e2tsr", | ||||
|         "--no-mutagen", | ||||
|         "--th-ff-jpg", | ||||
|         "--hist", | ||||
|         os.path.join(td, "dbm"), | ||||
|     ] | ||||
|     pdirs = [] | ||||
|     hpaths = {} | ||||
|  | ||||
|     for d1 in ["r", "w", "a"]: | ||||
|         pdirs.append("{}/{}".format(td, d1)) | ||||
|         pdirs.append("{}/{}/j".format(td, d1)) | ||||
|         for d2 in ["r", "w", "a"]: | ||||
|             d = os.path.join(td, d1, "j", d2) | ||||
|             pdirs.append(d) | ||||
|             os.makedirs(d) | ||||
|  | ||||
|     pdirs = [x.replace("\\", "/") for x in pdirs] | ||||
|     udirs = [x.split("/", 2)[2] for x in pdirs] | ||||
|     perms = [x.rstrip("j/")[-1] for x in pdirs] | ||||
|     for pd, ud, p in zip(pdirs, udirs, perms): | ||||
|         if ud[-1] == "j": | ||||
|             continue | ||||
|  | ||||
|         hp = None | ||||
|         if pd.endswith("st/a"): | ||||
|             hp = hpaths[ud] = os.path.join(td, "db1") | ||||
|         elif pd[:-1].endswith("a/j/"): | ||||
|             hpaths[ud] = os.path.join(td, "dbm") | ||||
|             hp = None | ||||
|         else: | ||||
|             hp = "-" | ||||
|             hpaths[ud] = os.path.join(pd, ".hist") | ||||
|  | ||||
|         arg = "{}:{}:{}".format(pd, ud, p, hp) | ||||
|         if hp: | ||||
|             arg += ":chist=" + hp | ||||
|  | ||||
|         args += ["-v", arg] | ||||
|  | ||||
|     # return | ||||
|     cpp = Cpp(args) | ||||
|     CPP.append(cpp) | ||||
|     cpp.await_idle(ub, 3) | ||||
|  | ||||
|     for d in udirs: | ||||
|         vid = ovid + "\n{}".format(d).encode("utf-8") | ||||
|         try: | ||||
|             requests.post(ub + d, data={"act": "bput"}, files={"f": ("a.h264", vid)}) | ||||
|         except: | ||||
|             pass | ||||
|  | ||||
|     cpp.clean() | ||||
|  | ||||
|     # GET permission | ||||
|     for d, p in zip(udirs, perms): | ||||
|         u = "{}{}/a.h264".format(ub, d) | ||||
|         r = requests.get(u) | ||||
|         ok = bool(r) | ||||
|         if ok != (p in ["a"]): | ||||
|             raise Exception("get {} with perm {} at {}".format(ok, p, u)) | ||||
|  | ||||
|     # stat filesystem | ||||
|     for d, p in zip(pdirs, perms): | ||||
|         u = "{}/a.h264".format(d) | ||||
|         ok = os.path.exists(u) | ||||
|         if ok != (p in ["a", "w"]): | ||||
|             raise Exception("stat {} with perm {} at {}".format(ok, p, u)) | ||||
|  | ||||
|     # GET thumbnail, vreify contents | ||||
|     for d, p in zip(udirs, perms): | ||||
|         u = "{}{}/a.h264?th=j".format(ub, d) | ||||
|         r = requests.get(u) | ||||
|         ok = bool(r and r.content[:3] == b"\xff\xd8\xff") | ||||
|         if ok != (p in ["a"]): | ||||
|             raise Exception("thumb {} with perm {} at {}".format(ok, p, u)) | ||||
|  | ||||
|     # check tags | ||||
|     cpp.await_idle(ub, 5) | ||||
|     for d, p in zip(udirs, perms): | ||||
|         u = "{}{}?ls".format(ub, d) | ||||
|         r = requests.get(u) | ||||
|         j = r.json() if r else False | ||||
|         tag = None | ||||
|         if j: | ||||
|             for f in j["files"]: | ||||
|                 tag = tag or f["tags"].get("res") | ||||
|  | ||||
|         r_ok = bool(j) | ||||
|         w_ok = bool(r_ok and j.get("files")) | ||||
|  | ||||
|         if not r_ok or w_ok != (p in ["a"]): | ||||
|             raise Exception("ls {} with perm {} at {}".format(ok, p, u)) | ||||
|  | ||||
|         if (tag and p != "a") or (not tag and p == "a"): | ||||
|             raise Exception("tag {} with perm {} at {}".format(tag, p, u)) | ||||
|  | ||||
|         if tag is not None and tag != "48x32": | ||||
|             raise Exception("tag [{}] at {}".format(tag, u)) | ||||
|  | ||||
|     cpp.stop(True) | ||||
|  | ||||
|  | ||||
| def run(tc): | ||||
|     try: | ||||
|         tc() | ||||
|     finally: | ||||
|         try: | ||||
|             CPP[0].stop(False) | ||||
|         except: | ||||
|             pass | ||||
|  | ||||
|  | ||||
| def main(): | ||||
|     run(tc1) | ||||
|  | ||||
|  | ||||
| if __name__ == "__main__": | ||||
|     main() | ||||
| @@ -28,6 +28,7 @@ class Cfg(Namespace): | ||||
|             a=a, | ||||
|             v=v, | ||||
|             c=c, | ||||
|             rproxy=0, | ||||
|             ed=False, | ||||
|             no_zip=False, | ||||
|             no_scandir=False, | ||||
| @@ -37,6 +38,9 @@ class Cfg(Namespace): | ||||
|             nih=True, | ||||
|             mtp=[], | ||||
|             mte="a", | ||||
|             hist=None, | ||||
|             no_hash=False, | ||||
|             css_browser=None, | ||||
|             **{k: False for k in "e2d e2ds e2dsa e2t e2ts e2tsr".split()} | ||||
|         ) | ||||
|  | ||||
| @@ -99,7 +103,7 @@ class TestHttpCli(unittest.TestCase): | ||||
|             pprint.pprint(vcfg) | ||||
|  | ||||
|             self.args = Cfg(v=vcfg, a=["o:o", "x:x"]) | ||||
|             self.auth = AuthSrv(self.args, self.log) | ||||
|             self.asrv = 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) | ||||
| @@ -188,12 +192,12 @@ class TestHttpCli(unittest.TestCase): | ||||
|     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) | ||||
|         conn = tu.VHttpConn(self.args, self.asrv, 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)) | ||||
|         conn = tu.VHttpConn(self.args, self.asrv, self.log, hdr(url)) | ||||
|         HttpCli(conn).run() | ||||
|         if binary: | ||||
|             h, b = conn.s._reply.split(b"\r\n\r\n", 1) | ||||
|   | ||||
| @@ -11,15 +11,22 @@ from textwrap import dedent | ||||
| from argparse import Namespace | ||||
|  | ||||
| from tests import util as tu | ||||
| from copyparty.authsrv import AuthSrv | ||||
| from copyparty.authsrv import AuthSrv, VFS | ||||
| from copyparty import util | ||||
|  | ||||
|  | ||||
| class Cfg(Namespace): | ||||
|     def __init__(self, a=[], v=[], c=None): | ||||
|         ex = {k: False for k in "e2d e2ds e2dsa e2t e2ts e2tsr".split()} | ||||
|         ex["mtp"] = [] | ||||
|         ex["mte"] = "a" | ||||
|         ex2 = { | ||||
|             "mtp": [], | ||||
|             "mte": "a", | ||||
|             "hist": None, | ||||
|             "no_hash": False, | ||||
|             "css_browser": None, | ||||
|             "rproxy": 0, | ||||
|         } | ||||
|         ex.update(ex2) | ||||
|         super(Cfg, self).__init__(a=a, v=v, c=c, **ex) | ||||
|  | ||||
|  | ||||
| @@ -47,6 +54,7 @@ class TestVFS(unittest.TestCase): | ||||
|         self.assertEqual(util.undot(query), response) | ||||
|  | ||||
|     def ls(self, vfs, vpath, uname): | ||||
|         # type: (VFS, str, str) -> tuple[str, str, str] | ||||
|         """helper for resolving and listing a folder""" | ||||
|         vn, rem = vfs.get(vpath, uname, True, False) | ||||
|         r1 = vn.ls(rem, uname, False) | ||||
| @@ -112,13 +120,13 @@ class TestVFS(unittest.TestCase): | ||||
|         n = vfs.nodes["a"] | ||||
|         self.assertEqual(len(vfs.nodes), 1) | ||||
|         self.assertEqual(n.vpath, "a") | ||||
|         self.assertEqual(n.realpath, td + "/a") | ||||
|         self.assertEqual(n.realpath, os.path.join(td, "a")) | ||||
|         self.assertEqual(n.uread, ["*", "k"]) | ||||
|         self.assertEqual(n.uwrite, ["k"]) | ||||
|         n = n.nodes["ac"] | ||||
|         self.assertEqual(len(vfs.nodes), 1) | ||||
|         self.assertEqual(n.vpath, "a/ac") | ||||
|         self.assertEqual(n.realpath, td + "/a/ac") | ||||
|         self.assertEqual(n.realpath, os.path.join(td, "a", "ac")) | ||||
|         self.assertEqual(n.uread, ["*", "k"]) | ||||
|         self.assertEqual(n.uwrite, ["k"]) | ||||
|         n = n.nodes["acb"] | ||||
| @@ -250,7 +258,7 @@ class TestVFS(unittest.TestCase): | ||||
|         n = au.vfs | ||||
|         # root was not defined, so PWD with no access to anyone | ||||
|         self.assertEqual(n.vpath, "") | ||||
|         self.assertEqual(n.realpath, td) | ||||
|         self.assertEqual(n.realpath, None) | ||||
|         self.assertEqual(n.uread, []) | ||||
|         self.assertEqual(n.uwrite, []) | ||||
|         self.assertEqual(len(n.nodes), 1) | ||||
|   | ||||
| @@ -60,7 +60,7 @@ def get_ramdisk(): | ||||
|  | ||||
|     if os.path.exists("/Volumes"): | ||||
|         # hdiutil eject /Volumes/cptd/ | ||||
|         devname, _ = chkcmd("hdiutil", "attach", "-nomount", "ram://65536") | ||||
|         devname, _ = chkcmd("hdiutil", "attach", "-nomount", "ram://131072") | ||||
|         devname = devname.strip() | ||||
|         print("devname: [{}]".format(devname)) | ||||
|         for _ in range(10): | ||||
| @@ -110,12 +110,13 @@ class VHttpSrv(object): | ||||
|  | ||||
|  | ||||
| class VHttpConn(object): | ||||
|     def __init__(self, args, auth, log, buf): | ||||
|     def __init__(self, args, asrv, 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.asrv = asrv | ||||
|         self.is_mp = False | ||||
|         self.log_func = log | ||||
|         self.log_src = "a" | ||||
|         self.lf_url = None | ||||
|   | ||||
		Reference in New Issue
	
	Block a user