mirror of
				https://github.com/9001/copyparty.git
				synced 2025-11-04 05:43:17 +00:00 
			
		
		
		
	Compare commits
	
		
			75 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					755a2ee023 | ||
| 
						 | 
					69d3359e47 | ||
| 
						 | 
					a90c49b8fb | ||
| 
						 | 
					b1222edb27 | ||
| 
						 | 
					b967a92f69 | ||
| 
						 | 
					90a5cb5e59 | ||
| 
						 | 
					7aba9cb76b | ||
| 
						 | 
					f550a8171d | ||
| 
						 | 
					82e568d4c9 | ||
| 
						 | 
					7b2a4a3d59 | ||
| 
						 | 
					0265455cd1 | ||
| 
						 | 
					afafc886a4 | ||
| 
						 | 
					8a959f6ac4 | ||
| 
						 | 
					1c3aa0d2c5 | ||
| 
						 | 
					79b7d3316a | ||
| 
						 | 
					fa7768583a | ||
| 
						 | 
					faf49f6c15 | ||
| 
						 | 
					765af31b83 | ||
| 
						 | 
					b6a3c52d67 | ||
| 
						 | 
					b025c2f660 | ||
| 
						 | 
					e559a7c878 | ||
| 
						 | 
					5c8855aafd | ||
| 
						 | 
					b5fc537b89 | ||
| 
						 | 
					14899d3a7c | ||
| 
						 | 
					0ea7881652 | ||
| 
						 | 
					ec29b59d1e | ||
| 
						 | 
					9405597c15 | ||
| 
						 | 
					82441978c6 | ||
| 
						 | 
					e0e6291bdb | ||
| 
						 | 
					b2b083fd0a | ||
| 
						 | 
					f8a51b68e7 | ||
| 
						 | 
					e0a19108e5 | ||
| 
						 | 
					770ea68ca8 | ||
| 
						 | 
					ce36c52baf | ||
| 
						 | 
					a7da1dd233 | ||
| 
						 | 
					678ef296b4 | ||
| 
						 | 
					9e5627d805 | ||
| 
						 | 
					5958ee4439 | ||
| 
						 | 
					7127e57f0e | ||
| 
						 | 
					ee9c6dc8aa | ||
| 
						 | 
					92779b3f48 | ||
| 
						 | 
					2f1baf17d4 | ||
| 
						 | 
					583da3d4a9 | ||
| 
						 | 
					bf9ff78bcc | ||
| 
						 | 
					2cb07792cc | ||
| 
						 | 
					47bc8bb466 | ||
| 
						 | 
					94ad1f5732 | ||
| 
						 | 
					09557fbe83 | ||
| 
						 | 
					1c0f44fa4e | ||
| 
						 | 
					fc4d59d2d7 | ||
| 
						 | 
					12345fbacc | ||
| 
						 | 
					2e33c8d222 | ||
| 
						 | 
					db5f07f164 | ||
| 
						 | 
					e050e69a43 | ||
| 
						 | 
					27cb1d4fc7 | ||
| 
						 | 
					5d6a740947 | ||
| 
						 | 
					da3f68c363 | ||
| 
						 | 
					d7d1c3685c | ||
| 
						 | 
					dab3407beb | ||
| 
						 | 
					592987a54a | ||
| 
						 | 
					8dca8326f7 | ||
| 
						 | 
					633481fae3 | ||
| 
						 | 
					e7b99e6fb7 | ||
| 
						 | 
					2a6a3aedd0 | ||
| 
						 | 
					866c74c841 | ||
| 
						 | 
					dad92bde26 | ||
| 
						 | 
					a994e034f7 | ||
| 
						 | 
					2801c04f2e | ||
| 
						 | 
					316e3abfab | ||
| 
						 | 
					c15ecb6c8e | ||
| 
						 | 
					ee96005026 | ||
| 
						 | 
					5b55d05a20 | ||
| 
						 | 
					2f09c62c4e | ||
| 
						 | 
					1cc8b873d4 | ||
| 
						 | 
					15d5859750 | 
							
								
								
									
										10
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -11,14 +11,12 @@ dist/
 | 
				
			|||||||
sfx/
 | 
					sfx/
 | 
				
			||||||
.venv/
 | 
					.venv/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# sublime
 | 
					# ide
 | 
				
			||||||
*.sublime-workspace
 | 
					*.sublime-workspace
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# winmerge
 | 
					# winmerge
 | 
				
			||||||
*.bak
 | 
					*.bak
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# other licenses
 | 
					# derived
 | 
				
			||||||
contrib/
 | 
					copyparty/web/deps/
 | 
				
			||||||
 | 
					srv/
 | 
				
			||||||
# deps
 | 
					 | 
				
			||||||
copyparty/web/deps
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										2
									
								
								.vscode/launch.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.vscode/launch.json
									
									
									
									
										vendored
									
									
								
							@@ -9,8 +9,6 @@
 | 
				
			|||||||
            "console": "integratedTerminal",
 | 
					            "console": "integratedTerminal",
 | 
				
			||||||
            "cwd": "${workspaceFolder}",
 | 
					            "cwd": "${workspaceFolder}",
 | 
				
			||||||
            "args": [
 | 
					            "args": [
 | 
				
			||||||
                "-j",
 | 
					 | 
				
			||||||
                "0",
 | 
					 | 
				
			||||||
                //"-nw",
 | 
					                //"-nw",
 | 
				
			||||||
                "-a",
 | 
					                "-a",
 | 
				
			||||||
                "ed:wark",
 | 
					                "ed:wark",
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										4
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							@@ -37,7 +37,7 @@
 | 
				
			|||||||
    "python.linting.banditEnabled": true,
 | 
					    "python.linting.banditEnabled": true,
 | 
				
			||||||
    "python.linting.flake8Args": [
 | 
					    "python.linting.flake8Args": [
 | 
				
			||||||
        "--max-line-length=120",
 | 
					        "--max-line-length=120",
 | 
				
			||||||
        "--ignore=E722,F405,E203,W503,W293",
 | 
					        "--ignore=E722,F405,E203,W503,W293,E402",
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
    "python.linting.banditArgs": [
 | 
					    "python.linting.banditArgs": [
 | 
				
			||||||
        "--ignore=B104"
 | 
					        "--ignore=B104"
 | 
				
			||||||
@@ -55,6 +55,6 @@
 | 
				
			|||||||
    //
 | 
					    //
 | 
				
			||||||
    //  things you may wanna edit:
 | 
					    //  things you may wanna edit:
 | 
				
			||||||
    //
 | 
					    //
 | 
				
			||||||
    "python.pythonPath": ".venv/bin/python",
 | 
					    "python.pythonPath": "/usr/bin/python3",
 | 
				
			||||||
    //"python.linting.enabled": true,
 | 
					    //"python.linting.enabled": true,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
							
								
								
									
										34
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										34
									
								
								README.md
									
									
									
									
									
								
							@@ -19,6 +19,8 @@ turn your phone or raspi into a portable file server with resumable uploads/down
 | 
				
			|||||||
* Android-Chrome: set max "parallel uploads" for 200% upload speed (android bug)
 | 
					* Android-Chrome: set max "parallel uploads" for 200% upload speed (android bug)
 | 
				
			||||||
* Android-Firefox: takes a while to select files (in order to avoid the above android-chrome issue)
 | 
					* Android-Firefox: takes a while to select files (in order to avoid the above android-chrome issue)
 | 
				
			||||||
* Desktop-Firefox: may use gigabytes of RAM if your connection is great and your files are massive
 | 
					* Desktop-Firefox: may use gigabytes of RAM if your connection is great and your files are massive
 | 
				
			||||||
 | 
					* paper-printing is affected by dark/light-mode! use lightmode for color, darkmode for grayscale
 | 
				
			||||||
 | 
					  * because no browsers currently implement the media-query to do this properly orz
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## status
 | 
					## status
 | 
				
			||||||
@@ -36,10 +38,22 @@ turn your phone or raspi into a portable file server with resumable uploads/down
 | 
				
			|||||||
* [x] accounts
 | 
					* [x] accounts
 | 
				
			||||||
* [x] markdown viewer
 | 
					* [x] markdown viewer
 | 
				
			||||||
* [x] markdown editor
 | 
					* [x] markdown editor
 | 
				
			||||||
 | 
					* [x] FUSE client (read-only)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
summary: it works! you can use it! (but technically not even close to beta)
 | 
					summary: it works! you can use it! (but technically not even close to beta)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# client examples
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					* javascript: dump some state into a file (two separate examples)
 | 
				
			||||||
 | 
					  * `await fetch('https://127.0.0.1:3923/', {method:"PUT", body: JSON.stringify(foo)});`
 | 
				
			||||||
 | 
					  * `var xhr = new XMLHttpRequest(); xhr.open('POST', 'https://127.0.0.1:3923/msgs?raw'); xhr.send('foo');`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					* FUSE: mount a copyparty server as a local filesystem
 | 
				
			||||||
 | 
					  * cross-platform python client available in [./bin/](bin/)
 | 
				
			||||||
 | 
					  * [rclone](https://rclone.org/) as client can give ~5x performance, see [./docs/rclone.md](docs/rclone.md)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# dependencies
 | 
					# dependencies
 | 
				
			||||||
 | 
					
 | 
				
			||||||
* `jinja2`
 | 
					* `jinja2`
 | 
				
			||||||
@@ -55,17 +69,23 @@ currently there are two self-contained binaries:
 | 
				
			|||||||
* `copyparty-sfx.sh` for unix (linux and osx) -- smaller, more robust
 | 
					* `copyparty-sfx.sh` for unix (linux and osx) -- smaller, more robust
 | 
				
			||||||
* `copyparty-sfx.py` for windows (unix too) -- crossplatform, beta
 | 
					* `copyparty-sfx.py` for windows (unix too) -- crossplatform, beta
 | 
				
			||||||
 | 
					
 | 
				
			||||||
launch either of them and it'll unpack and run copyparty, assuming you have python installed of course
 | 
					launch either of them (**use sfx.py on systemd**) and it'll unpack and run copyparty, assuming you have python installed of course
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pls note that `copyparty-sfx.sh` will fail if you rename `copyparty-sfx.py` to `copyparty.py` and keep it in the same folder because `sys.path` is funky
 | 
					pls note that `copyparty-sfx.sh` will fail if you rename `copyparty-sfx.py` to `copyparty.py` and keep it in the same folder because `sys.path` is funky
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if you don't need all the features you can repack the sfx and save a bunch of space, tho currently the only removable feature is the opus/vorbis javascript decoder which is needed by apple devices to play foss audio files
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
steps to reduce the sfx size from `720 kB` to `250 kB` roughly:
 | 
					## sfx repack
 | 
				
			||||||
* run one of the sfx'es once to unpack it
 | 
					 | 
				
			||||||
* `./scripts/make-sfx.sh re no-ogv` creates a new pair of sfx
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
no internet connection needed, just download an sfx and the repo zip (also if you're on windows use msys2)
 | 
					if you don't need all the features you can repack the sfx and save a bunch of space; all you need is an sfx and a copy of this repo (nothing else to download or build, except for either msys2 or WSL if you're on windows)
 | 
				
			||||||
 | 
					* `724K` original size as of v0.4.0
 | 
				
			||||||
 | 
					* `256K` after `./scripts/make-sfx.sh re no-ogv`
 | 
				
			||||||
 | 
					* `164K` after `./scripts/make-sfx.sh re no-ogv no-cm`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					the features you can opt to drop are
 | 
				
			||||||
 | 
					* `ogv`.js, the opus/vorbis decoder which is needed by apple devices to play foss audio files
 | 
				
			||||||
 | 
					* `cm`/easymde, the "fancy" markdown editor
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					for the `re`pack to work, first run one of the sfx'es once to unpack it
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# install on android
 | 
					# install on android
 | 
				
			||||||
@@ -104,10 +124,8 @@ in the `scripts` folder:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
roughly sorted by priority
 | 
					roughly sorted by priority
 | 
				
			||||||
 | 
					
 | 
				
			||||||
* sortable browser columns
 | 
					 | 
				
			||||||
* up2k handle filename too long
 | 
					* up2k handle filename too long
 | 
				
			||||||
* up2k fails on empty files? alert then stuck
 | 
					* up2k fails on empty files? alert then stuck
 | 
				
			||||||
* unexpected filepath on dupe up2k
 | 
					 | 
				
			||||||
* drop onto folders
 | 
					* drop onto folders
 | 
				
			||||||
* look into android thumbnail cache file format
 | 
					* look into android thumbnail cache file format
 | 
				
			||||||
* support pillow-simd
 | 
					* support pillow-simd
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										36
									
								
								bin/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								bin/README.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,36 @@
 | 
				
			|||||||
 | 
					# copyparty-fuse.py
 | 
				
			||||||
 | 
					* mount a copyparty server as a local filesystem (read-only)
 | 
				
			||||||
 | 
					* **supports Windows!** -- expect `194 MiB/s` sequential read
 | 
				
			||||||
 | 
					* **supports Linux** -- expect `117 MiB/s` sequential read
 | 
				
			||||||
 | 
					* **supports macos** -- expect `85 MiB/s` sequential read
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					filecache is default-on for windows and macos;
 | 
				
			||||||
 | 
					* macos readsize is 64kB, so speed ~32 MiB/s without the cache
 | 
				
			||||||
 | 
					* windows readsize varies by software; explorer=1M, pv=32k
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					note that copyparty should run with `-ed` to enable dotfiles (hidden otherwise)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					also consider using [../docs/rclone.md](../docs/rclone.md) instead for 5x performance
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## to run this on windows:
 | 
				
			||||||
 | 
					* install [winfsp](https://github.com/billziss-gh/winfsp/releases/latest) and [python 3](https://www.python.org/downloads/)
 | 
				
			||||||
 | 
					  * [x] add python 3.x to PATH (it asks during install)
 | 
				
			||||||
 | 
					* `python -m pip install --user fusepy`
 | 
				
			||||||
 | 
					* `python ./copyparty-fuse.py n: http://192.168.1.69:3923/`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					10% faster in [msys2](https://www.msys2.org/), 700% faster if debug prints are enabled:
 | 
				
			||||||
 | 
					* `pacman -S mingw64/mingw-w64-x86_64-python{,-pip}`
 | 
				
			||||||
 | 
					* `/mingw64/bin/python3 -m pip install --user fusepy`
 | 
				
			||||||
 | 
					* `/mingw64/bin/python3 ./copyparty-fuse.py [...]`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					you could replace winfsp with [dokan](https://github.com/dokan-dev/dokany/releases/latest), let me know if you [figure out how](https://github.com/dokan-dev/dokany/wiki/FUSE)  
 | 
				
			||||||
 | 
					(winfsp's sshfs leaks, doesn't look like winfsp itself does, should be fine)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# copyparty-fuse🅱️.py
 | 
				
			||||||
 | 
					* mount a copyparty server as a local filesystem (read-only)
 | 
				
			||||||
 | 
					* does the same thing except more correct, `samba` approves
 | 
				
			||||||
 | 
					* **supports Linux** -- expect `18 MiB/s` (wait what)
 | 
				
			||||||
 | 
					* **supports Macos** -- probably
 | 
				
			||||||
@@ -7,47 +7,83 @@ __copyright__ = 2019
 | 
				
			|||||||
__license__ = "MIT"
 | 
					__license__ = "MIT"
 | 
				
			||||||
__url__ = "https://github.com/9001/copyparty/"
 | 
					__url__ = "https://github.com/9001/copyparty/"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import re
 | 
					 | 
				
			||||||
import sys
 | 
					 | 
				
			||||||
import time
 | 
					 | 
				
			||||||
import stat
 | 
					 | 
				
			||||||
import errno
 | 
					 | 
				
			||||||
import struct
 | 
					 | 
				
			||||||
import threading
 | 
					 | 
				
			||||||
import http.client  # py2: httplib
 | 
					 | 
				
			||||||
import urllib.parse
 | 
					 | 
				
			||||||
from datetime import datetime
 | 
					 | 
				
			||||||
from urllib.parse import quote_from_bytes as quote
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
try:
 | 
					 | 
				
			||||||
    from fuse import FUSE, FuseOSError, Operations
 | 
					 | 
				
			||||||
except:
 | 
					 | 
				
			||||||
    print("\n    could not import fuse;\n    pip install fusepy\n")
 | 
					 | 
				
			||||||
    raise
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
mount a copyparty server (local or remote) as a filesystem
 | 
					mount a copyparty server (local or remote) as a filesystem
 | 
				
			||||||
 | 
					
 | 
				
			||||||
usage:
 | 
					usage:
 | 
				
			||||||
  python copyparty-fuse.py ./music http://192.168.1.69:1234/
 | 
					  python copyparty-fuse.py ./music http://192.168.1.69:3923/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
dependencies:
 | 
					dependencies:
 | 
				
			||||||
  sudo apk add fuse-dev
 | 
					  python3 -m pip install --user fusepy
 | 
				
			||||||
  python3 -m venv ~/pe/ve.fusepy
 | 
					  + on Linux: sudo apk add fuse
 | 
				
			||||||
  . ~/pe/ve.fusepy/bin/activate
 | 
					  + on Macos: https://osxfuse.github.io/
 | 
				
			||||||
  pip install fusepy
 | 
					  + on Windows: https://github.com/billziss-gh/winfsp/releases/latest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					get server cert:
 | 
				
			||||||
MB/s
 | 
					  awk '/-BEGIN CERTIFICATE-/ {a=1} a; /-END CERTIFICATE-/{exit}' <(openssl s_client -connect 127.0.0.1:3923 </dev/null 2>/dev/null) >cert.pem
 | 
				
			||||||
 28   cache NOthread
 | 
					 | 
				
			||||||
 24   cache   thread
 | 
					 | 
				
			||||||
 29   cache NOthread NOmutex
 | 
					 | 
				
			||||||
 67 NOcache NOthread NOmutex  ( ´・ω・) nyoro~n
 | 
					 | 
				
			||||||
 10 NOcache   thread NOmutex
 | 
					 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import re
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
 | 
					import sys
 | 
				
			||||||
 | 
					import time
 | 
				
			||||||
 | 
					import stat
 | 
				
			||||||
 | 
					import errno
 | 
				
			||||||
 | 
					import struct
 | 
				
			||||||
 | 
					import codecs
 | 
				
			||||||
 | 
					import builtins
 | 
				
			||||||
 | 
					import platform
 | 
				
			||||||
 | 
					import argparse
 | 
				
			||||||
 | 
					import threading
 | 
				
			||||||
 | 
					import traceback
 | 
				
			||||||
 | 
					import http.client  # py2: httplib
 | 
				
			||||||
 | 
					import urllib.parse
 | 
				
			||||||
 | 
					from datetime import datetime
 | 
				
			||||||
 | 
					from urllib.parse import quote_from_bytes as quote
 | 
				
			||||||
 | 
					from urllib.parse import unquote_to_bytes as unquote
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					WINDOWS = sys.platform == "win32"
 | 
				
			||||||
 | 
					MACOS = platform.system() == "Darwin"
 | 
				
			||||||
 | 
					info = log = dbg = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					try:
 | 
				
			||||||
 | 
					    from fuse import FUSE, FuseOSError, Operations
 | 
				
			||||||
 | 
					except:
 | 
				
			||||||
 | 
					    if WINDOWS:
 | 
				
			||||||
 | 
					        libfuse = "install https://github.com/billziss-gh/winfsp/releases/latest"
 | 
				
			||||||
 | 
					    elif MACOS:
 | 
				
			||||||
 | 
					        libfuse = "install https://osxfuse.github.io/"
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        libfuse = "apt install libfuse\n    modprobe fuse"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    print(
 | 
				
			||||||
 | 
					        "\n  could not import fuse; these may help:"
 | 
				
			||||||
 | 
					        + "\n    python3 -m pip install --user fusepy\n    "
 | 
				
			||||||
 | 
					        + libfuse
 | 
				
			||||||
 | 
					        + "\n"
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    raise
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def print(*args, **kwargs):
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        builtins.print(*list(args), **kwargs)
 | 
				
			||||||
 | 
					    except:
 | 
				
			||||||
 | 
					        builtins.print(termsafe(" ".join(str(x) for x in args)), **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def termsafe(txt):
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        return txt.encode(sys.stdout.encoding, "backslashreplace").decode(
 | 
				
			||||||
 | 
					            sys.stdout.encoding
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    except:
 | 
				
			||||||
 | 
					        return txt.encode(sys.stdout.encoding, "replace").decode(sys.stdout.encoding)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def threadless_log(msg):
 | 
					def threadless_log(msg):
 | 
				
			||||||
    print(msg + "\n", end="")
 | 
					    print(msg + "\n", end="")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -60,27 +96,127 @@ def boring_log(msg):
 | 
				
			|||||||
def rice_tid():
 | 
					def rice_tid():
 | 
				
			||||||
    tid = threading.current_thread().ident
 | 
					    tid = threading.current_thread().ident
 | 
				
			||||||
    c = struct.unpack(b"B" * 5, struct.pack(b">Q", tid)[-5:])
 | 
					    c = struct.unpack(b"B" * 5, struct.pack(b">Q", tid)[-5:])
 | 
				
			||||||
    return "".join("\033[1;37;48;5;{}m{:02x}".format(x, x) for x in c)
 | 
					    return "".join("\033[1;37;48;5;{}m{:02x}".format(x, x) for x in c) + "\033[0m"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def fancy_log(msg):
 | 
					def fancy_log(msg):
 | 
				
			||||||
    print("{}\033[0m {}\n".format(rice_tid(), msg), end="")
 | 
					    print("{} {}\n".format(rice_tid(), msg), end="")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def null_log(msg):
 | 
					def null_log(msg):
 | 
				
			||||||
    pass
 | 
					    pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
log = boring_log
 | 
					def hexler(binary):
 | 
				
			||||||
log = fancy_log
 | 
					    return binary.replace("\r", "\\r").replace("\n", "\\n")
 | 
				
			||||||
log = threadless_log
 | 
					    return " ".join(["{}\033[36m{:02x}\033[0m".format(b, ord(b)) for b in binary])
 | 
				
			||||||
dbg = null_log
 | 
					    return " ".join(map(lambda b: format(ord(b), "02x"), binary))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def register_wtf8():
 | 
				
			||||||
 | 
					    def wtf8_enc(text):
 | 
				
			||||||
 | 
					        return str(text).encode("utf-8", "surrogateescape"), len(text)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def wtf8_dec(binary):
 | 
				
			||||||
 | 
					        return bytes(binary).decode("utf-8", "surrogateescape"), len(binary)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def wtf8_search(encoding_name):
 | 
				
			||||||
 | 
					        return codecs.CodecInfo(wtf8_enc, wtf8_dec, name="wtf-8")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    codecs.register(wtf8_search)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					bad_good = {}
 | 
				
			||||||
 | 
					good_bad = {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def enwin(txt):
 | 
				
			||||||
 | 
					    return "".join([bad_good.get(x, x) for x in txt])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for bad, good in bad_good.items():
 | 
				
			||||||
 | 
					        txt = txt.replace(bad, good)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return txt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def dewin(txt):
 | 
				
			||||||
 | 
					    return "".join([good_bad.get(x, x) for x in txt])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for bad, good in bad_good.items():
 | 
				
			||||||
 | 
					        txt = txt.replace(good, bad)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return txt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class RecentLog(object):
 | 
				
			||||||
 | 
					    def __init__(self):
 | 
				
			||||||
 | 
					        self.mtx = threading.Lock()
 | 
				
			||||||
 | 
					        self.f = None  # open("copyparty-fuse.log", "wb")
 | 
				
			||||||
 | 
					        self.q = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        thr = threading.Thread(target=self.printer)
 | 
				
			||||||
 | 
					        thr.daemon = True
 | 
				
			||||||
 | 
					        thr.start()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def put(self, msg):
 | 
				
			||||||
 | 
					        msg = "{} {}\n".format(rice_tid(), msg)
 | 
				
			||||||
 | 
					        if self.f:
 | 
				
			||||||
 | 
					            fmsg = " ".join([datetime.utcnow().strftime("%H%M%S.%f"), str(msg)])
 | 
				
			||||||
 | 
					            self.f.write(fmsg.encode("utf-8"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        with self.mtx:
 | 
				
			||||||
 | 
					            self.q.append(msg)
 | 
				
			||||||
 | 
					            if len(self.q) > 200:
 | 
				
			||||||
 | 
					                self.q = self.q[-50:]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def printer(self):
 | 
				
			||||||
 | 
					        while True:
 | 
				
			||||||
 | 
					            time.sleep(0.05)
 | 
				
			||||||
 | 
					            with self.mtx:
 | 
				
			||||||
 | 
					                q = self.q
 | 
				
			||||||
 | 
					                if not q:
 | 
				
			||||||
 | 
					                    continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                self.q = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            print("".join(q), end="")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# [windows/cmd/cpy3]  python dev\copyparty\bin\copyparty-fuse.py q: http://192.168.1.159:1234/
 | 
				
			||||||
 | 
					# [windows/cmd/msys2] C:\msys64\mingw64\bin\python3 dev\copyparty\bin\copyparty-fuse.py q: http://192.168.1.159:1234/
 | 
				
			||||||
 | 
					# [windows/mty/msys2] /mingw64/bin/python3 /c/Users/ed/dev/copyparty/bin/copyparty-fuse.py q: http://192.168.1.159:1234/
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# [windows] find /q/music/albums/Phant*24bit -printf '%s %p\n' | sort -n | tail -n 8 | sed -r 's/^[0-9]+ //' | while IFS= read -r x; do dd if="$x" of=/dev/null bs=4k count=8192 & done
 | 
				
			||||||
 | 
					# [alpine]  ll t; for x in t/2020_0724_16{2,3}*; do dd if="$x" of=/dev/null bs=4k count=10240 & done
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					#  72.4983 windows mintty msys2 fancy_log
 | 
				
			||||||
 | 
					# 219.5781 windows cmd msys2 fancy_log
 | 
				
			||||||
 | 
					# nope.avi windows cmd cpy3 fancy_log
 | 
				
			||||||
 | 
					#   9.8817 windows mintty msys2 RecentLog 200 50 0.1
 | 
				
			||||||
 | 
					#  10.2241 windows cmd cpy3 RecentLog 200 50 0.1
 | 
				
			||||||
 | 
					#   9.8494 windows cmd msys2 RecentLog 200 50 0.1
 | 
				
			||||||
 | 
					#   7.8061 windows mintty msys2 fancy_log <info-only>
 | 
				
			||||||
 | 
					#   7.9961 windows mintty msys2 RecentLog <info-only>
 | 
				
			||||||
 | 
					#   4.2603 alpine xfce4 cpy3 RecentLog
 | 
				
			||||||
 | 
					#   4.1538 alpine xfce4 cpy3 fancy_log
 | 
				
			||||||
 | 
					#   3.1742 alpine urxvt cpy3 fancy_log
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def get_tid():
 | 
					def get_tid():
 | 
				
			||||||
    return threading.current_thread().ident
 | 
					    return threading.current_thread().ident
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def html_dec(txt):
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					        txt.replace("<", "<")
 | 
				
			||||||
 | 
					        .replace(">", ">")
 | 
				
			||||||
 | 
					        .replace(""", '"')
 | 
				
			||||||
 | 
					        .replace("
", "\r")
 | 
				
			||||||
 | 
					        .replace("
", "\n")
 | 
				
			||||||
 | 
					        .replace("&", "&")
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CacheNode(object):
 | 
					class CacheNode(object):
 | 
				
			||||||
    def __init__(self, tag, data):
 | 
					    def __init__(self, tag, data):
 | 
				
			||||||
        self.tag = tag
 | 
					        self.tag = tag
 | 
				
			||||||
@@ -89,10 +225,11 @@ class CacheNode(object):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Gateway(object):
 | 
					class Gateway(object):
 | 
				
			||||||
    def __init__(self, base_url):
 | 
					    def __init__(self, ar):
 | 
				
			||||||
        self.base_url = base_url
 | 
					        self.base_url = ar.base_url
 | 
				
			||||||
 | 
					        self.password = ar.a
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        ui = urllib.parse.urlparse(base_url)
 | 
					        ui = urllib.parse.urlparse(self.base_url)
 | 
				
			||||||
        self.web_root = ui.path.strip("/")
 | 
					        self.web_root = ui.path.strip("/")
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            self.web_host, self.web_port = ui.netloc.split(":")
 | 
					            self.web_host, self.web_port = ui.netloc.split(":")
 | 
				
			||||||
@@ -102,15 +239,25 @@ class Gateway(object):
 | 
				
			|||||||
            if ui.scheme == "http":
 | 
					            if ui.scheme == "http":
 | 
				
			||||||
                self.web_port = 80
 | 
					                self.web_port = 80
 | 
				
			||||||
            elif ui.scheme == "https":
 | 
					            elif ui.scheme == "https":
 | 
				
			||||||
                raise Exception("todo")
 | 
					                self.web_port = 443
 | 
				
			||||||
            else:
 | 
					            else:
 | 
				
			||||||
                raise Exception("bad url?")
 | 
					                raise Exception("bad url?")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.ssl_context = None
 | 
				
			||||||
 | 
					        self.use_tls = ui.scheme.lower() == "https"
 | 
				
			||||||
 | 
					        if self.use_tls:
 | 
				
			||||||
 | 
					            import ssl
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if ar.td:
 | 
				
			||||||
 | 
					                self.ssl_context = ssl._create_unverified_context()
 | 
				
			||||||
 | 
					            elif ar.te:
 | 
				
			||||||
 | 
					                self.ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS)
 | 
				
			||||||
 | 
					                self.ssl_context.load_verify_locations(ar.te)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.conns = {}
 | 
					        self.conns = {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def quotep(self, path):
 | 
					    def quotep(self, path):
 | 
				
			||||||
        # TODO: mojibake support
 | 
					        path = path.encode("wtf-8")
 | 
				
			||||||
        path = path.encode("utf-8", "ignore")
 | 
					 | 
				
			||||||
        return quote(path, safe="/")
 | 
					        return quote(path, safe="/")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def getconn(self, tid=None):
 | 
					    def getconn(self, tid=None):
 | 
				
			||||||
@@ -118,9 +265,17 @@ class Gateway(object):
 | 
				
			|||||||
        try:
 | 
					        try:
 | 
				
			||||||
            return self.conns[tid]
 | 
					            return self.conns[tid]
 | 
				
			||||||
        except:
 | 
					        except:
 | 
				
			||||||
            log("new conn [{}] [{}]".format(self.web_host, self.web_port))
 | 
					            info("new conn [{}] [{}]".format(self.web_host, self.web_port))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            conn = http.client.HTTPConnection(self.web_host, self.web_port, timeout=260)
 | 
					            args = {}
 | 
				
			||||||
 | 
					            if not self.use_tls:
 | 
				
			||||||
 | 
					                C = http.client.HTTPConnection
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                C = http.client.HTTPSConnection
 | 
				
			||||||
 | 
					                if self.ssl_context:
 | 
				
			||||||
 | 
					                    args = {"context": self.ssl_context}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            conn = C(self.web_host, self.web_port, timeout=260, **args)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            self.conns[tid] = conn
 | 
					            self.conns[tid] = conn
 | 
				
			||||||
            return conn
 | 
					            return conn
 | 
				
			||||||
@@ -133,42 +288,75 @@ class Gateway(object):
 | 
				
			|||||||
        except:
 | 
					        except:
 | 
				
			||||||
            pass
 | 
					            pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def sendreq(self, *args, **kwargs):
 | 
					    def sendreq(self, *args, headers={}, **kwargs):
 | 
				
			||||||
        tid = get_tid()
 | 
					        tid = get_tid()
 | 
				
			||||||
 | 
					        if self.password:
 | 
				
			||||||
 | 
					            headers["Cookie"] = "=".join(["cppwd", self.password])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            c = self.getconn(tid)
 | 
					            c = self.getconn(tid)
 | 
				
			||||||
            c.request(*list(args), **kwargs)
 | 
					            c.request(*list(args), headers=headers, **kwargs)
 | 
				
			||||||
            return c.getresponse()
 | 
					            return c.getresponse()
 | 
				
			||||||
        except:
 | 
					        except:
 | 
				
			||||||
            self.closeconn(tid)
 | 
					            dbg("bad conn")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.closeconn(tid)
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
            c = self.getconn(tid)
 | 
					            c = self.getconn(tid)
 | 
				
			||||||
            c.request(*list(args), **kwargs)
 | 
					            c.request(*list(args), headers=headers, **kwargs)
 | 
				
			||||||
            return c.getresponse()
 | 
					            return c.getresponse()
 | 
				
			||||||
 | 
					        except:
 | 
				
			||||||
 | 
					            info("http connection failed:\n" + traceback.format_exc())
 | 
				
			||||||
 | 
					            if self.use_tls and not self.ssl_context:
 | 
				
			||||||
 | 
					                import ssl
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                cert = ssl.get_server_certificate((self.web_host, self.web_port))
 | 
				
			||||||
 | 
					                info("server certificate probably not trusted:\n" + cert)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            raise
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def listdir(self, path):
 | 
					    def listdir(self, path):
 | 
				
			||||||
        web_path = "/" + "/".join([self.web_root, path])
 | 
					        if bad_good:
 | 
				
			||||||
 | 
					            path = dewin(path)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        r = self.sendreq("GET", self.quotep(web_path))
 | 
					        web_path = self.quotep("/" + "/".join([self.web_root, path])) + "?dots"
 | 
				
			||||||
 | 
					        r = self.sendreq("GET", web_path)
 | 
				
			||||||
        if r.status != 200:
 | 
					        if r.status != 200:
 | 
				
			||||||
            self.closeconn()
 | 
					            self.closeconn()
 | 
				
			||||||
            raise Exception(
 | 
					            log(
 | 
				
			||||||
                "http error {} reading dir {} in {:x}".format(
 | 
					                "http error {} reading dir {} in {}".format(
 | 
				
			||||||
                    r.status, web_path, rice_tid()
 | 
					                    r.status, web_path, rice_tid()
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					            raise FuseOSError(errno.ENOENT)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return self.parse_html(r)
 | 
					        if not r.getheader("Content-Type", "").startswith("text/html"):
 | 
				
			||||||
 | 
					            log("listdir on file: {}".format(path))
 | 
				
			||||||
 | 
					            raise FuseOSError(errno.ENOENT)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            return self.parse_html(r)
 | 
				
			||||||
 | 
					        except:
 | 
				
			||||||
 | 
					            info(repr(path) + "\n" + traceback.format_exc())
 | 
				
			||||||
 | 
					            raise
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def download_file_range(self, path, ofs1, ofs2):
 | 
					    def download_file_range(self, path, ofs1, ofs2):
 | 
				
			||||||
        web_path = "/" + "/".join([self.web_root, path])
 | 
					        if bad_good:
 | 
				
			||||||
        hdr_range = "bytes={}-{}".format(ofs1, ofs2)
 | 
					            path = dewin(path)
 | 
				
			||||||
        log("downloading {}".format(hdr_range))
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        r = self.sendreq("GET", self.quotep(web_path), headers={"Range": hdr_range})
 | 
					        web_path = self.quotep("/" + "/".join([self.web_root, path])) + "?raw"
 | 
				
			||||||
 | 
					        hdr_range = "bytes={}-{}".format(ofs1, ofs2 - 1)
 | 
				
			||||||
 | 
					        info(
 | 
				
			||||||
 | 
					            "DL {:4.0f}K\033[36m{:>9}-{:<9}\033[0m{}".format(
 | 
				
			||||||
 | 
					                (ofs2 - ofs1) / 1024.0, ofs1, ofs2 - 1, hexler(path)
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        r = self.sendreq("GET", web_path, headers={"Range": hdr_range})
 | 
				
			||||||
        if r.status != http.client.PARTIAL_CONTENT:
 | 
					        if r.status != http.client.PARTIAL_CONTENT:
 | 
				
			||||||
            self.closeconn()
 | 
					            self.closeconn()
 | 
				
			||||||
            raise Exception(
 | 
					            raise Exception(
 | 
				
			||||||
                "http error {} reading file {} range {} in {:x}".format(
 | 
					                "http error {} reading file {} range {} in {}".format(
 | 
				
			||||||
                    r.status, web_path, hdr_range, rice_tid()
 | 
					                    r.status, web_path, hdr_range, rice_tid()
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
@@ -179,7 +367,7 @@ class Gateway(object):
 | 
				
			|||||||
        ret = []
 | 
					        ret = []
 | 
				
			||||||
        remainder = b""
 | 
					        remainder = b""
 | 
				
			||||||
        ptn = re.compile(
 | 
					        ptn = re.compile(
 | 
				
			||||||
            r"^<tr><td>(-|DIR)</td><td><a [^>]+>([^<]+)</a></td><td>([^<]+)</td><td>([^<]+)</td></tr>$"
 | 
					            r'^<tr><td>(-|DIR)</td><td><a[^>]* href="([^"]+)"[^>]*>([^<]+)</a></td><td>([^<]+)</td><td>([^<]+)</td></tr>$'
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        while True:
 | 
					        while True:
 | 
				
			||||||
@@ -201,9 +389,22 @@ class Gateway(object):
 | 
				
			|||||||
                    # print(line)
 | 
					                    # print(line)
 | 
				
			||||||
                    continue
 | 
					                    continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                ftype, fname, fsize, fdate = m.groups()
 | 
					                ftype, furl, fname, fsize, fdate = m.groups()
 | 
				
			||||||
                ts = datetime.strptime(fdate, "%Y-%m-%d %H:%M:%S").timestamp()
 | 
					                fname = furl.rstrip("/").split("/")[-1]
 | 
				
			||||||
                sz = int(fsize)
 | 
					                fname = unquote(fname)
 | 
				
			||||||
 | 
					                fname = fname.decode("wtf-8")
 | 
				
			||||||
 | 
					                if bad_good:
 | 
				
			||||||
 | 
					                    fname = enwin(fname)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                sz = 1
 | 
				
			||||||
 | 
					                ts = 60 * 60 * 24 * 2
 | 
				
			||||||
 | 
					                try:
 | 
				
			||||||
 | 
					                    sz = int(fsize)
 | 
				
			||||||
 | 
					                    ts = datetime.strptime(fdate, "%Y-%m-%d %H:%M:%S").timestamp()
 | 
				
			||||||
 | 
					                except:
 | 
				
			||||||
 | 
					                    info("bad HTML or OS [{}] [{}]".format(fdate, fsize))
 | 
				
			||||||
 | 
					                    # python cannot strptime(1959-01-01) on windows
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if ftype == "-":
 | 
					                if ftype == "-":
 | 
				
			||||||
                    ret.append([fname, self.stat_file(ts, sz), 0])
 | 
					                    ret.append([fname, self.stat_file(ts, sz), 0])
 | 
				
			||||||
                else:
 | 
					                else:
 | 
				
			||||||
@@ -213,7 +414,7 @@ class Gateway(object):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def stat_dir(self, ts, sz=4096):
 | 
					    def stat_dir(self, ts, sz=4096):
 | 
				
			||||||
        return {
 | 
					        return {
 | 
				
			||||||
            "st_mode": 0o555 | stat.S_IFDIR,
 | 
					            "st_mode": stat.S_IFDIR | 0o555,
 | 
				
			||||||
            "st_uid": 1000,
 | 
					            "st_uid": 1000,
 | 
				
			||||||
            "st_gid": 1000,
 | 
					            "st_gid": 1000,
 | 
				
			||||||
            "st_size": sz,
 | 
					            "st_size": sz,
 | 
				
			||||||
@@ -225,7 +426,7 @@ class Gateway(object):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def stat_file(self, ts, sz):
 | 
					    def stat_file(self, ts, sz):
 | 
				
			||||||
        return {
 | 
					        return {
 | 
				
			||||||
            "st_mode": 0o444 | stat.S_IFREG,
 | 
					            "st_mode": stat.S_IFREG | 0o444,
 | 
				
			||||||
            "st_uid": 1000,
 | 
					            "st_uid": 1000,
 | 
				
			||||||
            "st_gid": 1000,
 | 
					            "st_gid": 1000,
 | 
				
			||||||
            "st_size": sz,
 | 
					            "st_size": sz,
 | 
				
			||||||
@@ -237,8 +438,11 @@ class Gateway(object):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CPPF(Operations):
 | 
					class CPPF(Operations):
 | 
				
			||||||
    def __init__(self, base_url):
 | 
					    def __init__(self, ar):
 | 
				
			||||||
        self.gw = Gateway(base_url)
 | 
					        self.gw = Gateway(ar)
 | 
				
			||||||
 | 
					        self.junk_fh_ctr = 3
 | 
				
			||||||
 | 
					        self.n_dircache = ar.cd
 | 
				
			||||||
 | 
					        self.n_filecache = ar.cf
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.dircache = []
 | 
					        self.dircache = []
 | 
				
			||||||
        self.dircache_mtx = threading.Lock()
 | 
					        self.dircache_mtx = threading.Lock()
 | 
				
			||||||
@@ -246,14 +450,29 @@ class CPPF(Operations):
 | 
				
			|||||||
        self.filecache = []
 | 
					        self.filecache = []
 | 
				
			||||||
        self.filecache_mtx = threading.Lock()
 | 
					        self.filecache_mtx = threading.Lock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        log("up")
 | 
					        info("up")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _describe(self):
 | 
				
			||||||
 | 
					        msg = ""
 | 
				
			||||||
 | 
					        with self.filecache_mtx:
 | 
				
			||||||
 | 
					            for n, cn in enumerate(self.filecache):
 | 
				
			||||||
 | 
					                cache_path, cache1 = cn.tag
 | 
				
			||||||
 | 
					                cache2 = cache1 + len(cn.data)
 | 
				
			||||||
 | 
					                msg += "\n{:<2} {:>7} {:>10}:{:<9} {}".format(
 | 
				
			||||||
 | 
					                    n,
 | 
				
			||||||
 | 
					                    len(cn.data),
 | 
				
			||||||
 | 
					                    cache1,
 | 
				
			||||||
 | 
					                    cache2,
 | 
				
			||||||
 | 
					                    cache_path.replace("\r", "\\r").replace("\n", "\\n"),
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					        return msg
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def clean_dircache(self):
 | 
					    def clean_dircache(self):
 | 
				
			||||||
        """not threadsafe"""
 | 
					        """not threadsafe"""
 | 
				
			||||||
        now = time.time()
 | 
					        now = time.time()
 | 
				
			||||||
        cutoff = 0
 | 
					        cutoff = 0
 | 
				
			||||||
        for cn in self.dircache:
 | 
					        for cn in self.dircache:
 | 
				
			||||||
            if cn.ts - now > 1:
 | 
					            if now - cn.ts > self.n_dircache:
 | 
				
			||||||
                cutoff += 1
 | 
					                cutoff += 1
 | 
				
			||||||
            else:
 | 
					            else:
 | 
				
			||||||
                break
 | 
					                break
 | 
				
			||||||
@@ -262,8 +481,7 @@ class CPPF(Operations):
 | 
				
			|||||||
            self.dircache = self.dircache[cutoff:]
 | 
					            self.dircache = self.dircache[cutoff:]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_cached_dir(self, dirpath):
 | 
					    def get_cached_dir(self, dirpath):
 | 
				
			||||||
        # with self.dircache_mtx:
 | 
					        with self.dircache_mtx:
 | 
				
			||||||
        if True:
 | 
					 | 
				
			||||||
            self.clean_dircache()
 | 
					            self.clean_dircache()
 | 
				
			||||||
            for cn in self.dircache:
 | 
					            for cn in self.dircache:
 | 
				
			||||||
                if cn.tag == dirpath:
 | 
					                if cn.tag == dirpath:
 | 
				
			||||||
@@ -300,9 +518,8 @@ class CPPF(Operations):
 | 
				
			|||||||
        car = None
 | 
					        car = None
 | 
				
			||||||
        cdr = None
 | 
					        cdr = None
 | 
				
			||||||
        ncn = -1
 | 
					        ncn = -1
 | 
				
			||||||
        # with self.filecache_mtx:
 | 
					        dbg("cache request {}:{} |{}|".format(get1, get2, file_sz) + self._describe())
 | 
				
			||||||
        if True:
 | 
					        with self.filecache_mtx:
 | 
				
			||||||
            dbg("cache request from {} to {}, size {}".format(get1, get2, file_sz))
 | 
					 | 
				
			||||||
            for cn in self.filecache:
 | 
					            for cn in self.filecache:
 | 
				
			||||||
                ncn += 1
 | 
					                ncn += 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -312,6 +529,12 @@ class CPPF(Operations):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
                cache2 = cache1 + len(cn.data)
 | 
					                cache2 = cache1 + len(cn.data)
 | 
				
			||||||
                if get2 <= cache1 or get1 >= cache2:
 | 
					                if get2 <= cache1 or get1 >= cache2:
 | 
				
			||||||
 | 
					                    # request does not overlap with cached area at all
 | 
				
			||||||
 | 
					                    continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if get1 < cache1 and get2 > cache2:
 | 
				
			||||||
 | 
					                    # cached area does overlap, but must specifically contain
 | 
				
			||||||
 | 
					                    # either the first or last byte in the requested range
 | 
				
			||||||
                    continue
 | 
					                    continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if get1 >= cache1 and get2 <= cache2:
 | 
					                if get1 >= cache1 and get2 <= cache2:
 | 
				
			||||||
@@ -322,7 +545,7 @@ class CPPF(Operations):
 | 
				
			|||||||
                    buf_ofs = get1 - cache1
 | 
					                    buf_ofs = get1 - cache1
 | 
				
			||||||
                    buf_end = buf_ofs + (get2 - get1)
 | 
					                    buf_end = buf_ofs + (get2 - get1)
 | 
				
			||||||
                    dbg(
 | 
					                    dbg(
 | 
				
			||||||
                        "found all ({}, {} to {}, len {}) [{}:{}] = {}".format(
 | 
					                        "found all (#{} {}:{} |{}|) [{}:{}] = {}".format(
 | 
				
			||||||
                            ncn,
 | 
					                            ncn,
 | 
				
			||||||
                            cache1,
 | 
					                            cache1,
 | 
				
			||||||
                            cache2,
 | 
					                            cache2,
 | 
				
			||||||
@@ -334,11 +557,11 @@ class CPPF(Operations):
 | 
				
			|||||||
                    )
 | 
					                    )
 | 
				
			||||||
                    return cn.data[buf_ofs:buf_end]
 | 
					                    return cn.data[buf_ofs:buf_end]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if get2 < cache2:
 | 
					                if get2 <= cache2:
 | 
				
			||||||
                    x = cn.data[: get2 - cache1]
 | 
					                    x = cn.data[: get2 - cache1]
 | 
				
			||||||
                    if not cdr or len(cdr) < len(x):
 | 
					                    if not cdr or len(cdr) < len(x):
 | 
				
			||||||
                        dbg(
 | 
					                        dbg(
 | 
				
			||||||
                            "found car ({}, {} to {}, len {}) [:{}-{}] = [:{}] = {}".format(
 | 
					                            "found cdr (#{} {}:{} |{}|) [:{}-{}] = [:{}] = {}".format(
 | 
				
			||||||
                                ncn,
 | 
					                                ncn,
 | 
				
			||||||
                                cache1,
 | 
					                                cache1,
 | 
				
			||||||
                                cache2,
 | 
					                                cache2,
 | 
				
			||||||
@@ -353,11 +576,11 @@ class CPPF(Operations):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
                    continue
 | 
					                    continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if get1 > cache1:
 | 
					                if get1 >= cache1:
 | 
				
			||||||
                    x = cn.data[-(cache2 - get1) :]
 | 
					                    x = cn.data[-(max(0, cache2 - get1)) :]
 | 
				
			||||||
                    if not car or len(car) < len(x):
 | 
					                    if not car or len(car) < len(x):
 | 
				
			||||||
                        dbg(
 | 
					                        dbg(
 | 
				
			||||||
                            "found cdr ({}, {} to {}, len {}) [-({}-{}):] = [-{}:] = {}".format(
 | 
					                            "found car (#{} {}:{} |{}|) [-({}-{}):] = [-{}:] = {}".format(
 | 
				
			||||||
                                ncn,
 | 
					                                ncn,
 | 
				
			||||||
                                cache1,
 | 
					                                cache1,
 | 
				
			||||||
                                cache2,
 | 
					                                cache2,
 | 
				
			||||||
@@ -372,38 +595,52 @@ class CPPF(Operations):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
                    continue
 | 
					                    continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                raise Exception("what")
 | 
					                msg = "cache fallthrough\n{} {} {}\n{} {} {}\n{} {} --\n".format(
 | 
				
			||||||
 | 
					                    get1,
 | 
				
			||||||
 | 
					                    get2,
 | 
				
			||||||
 | 
					                    get2 - get1,
 | 
				
			||||||
 | 
					                    cache1,
 | 
				
			||||||
 | 
					                    cache2,
 | 
				
			||||||
 | 
					                    cache2 - cache1,
 | 
				
			||||||
 | 
					                    get1 - cache1,
 | 
				
			||||||
 | 
					                    get2 - cache2,
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                msg += self._describe()
 | 
				
			||||||
 | 
					                raise Exception(msg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if car and cdr:
 | 
					        if car and cdr and len(car) + len(cdr) == get2 - get1:
 | 
				
			||||||
            dbg("<cache> have both")
 | 
					            dbg("<cache> have both")
 | 
				
			||||||
 | 
					            return car + cdr
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            ret = car + cdr
 | 
					        elif cdr and (not car or len(car) < len(cdr)):
 | 
				
			||||||
            if len(ret) == get2 - get1:
 | 
					 | 
				
			||||||
                return ret
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            raise Exception("{} + {} != {} - {}".format(len(car), len(cdr), get2, get1))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        elif cdr:
 | 
					 | 
				
			||||||
            h_end = get1 + (get2 - get1) - len(cdr)
 | 
					            h_end = get1 + (get2 - get1) - len(cdr)
 | 
				
			||||||
            h_ofs = h_end - 512 * 1024
 | 
					            h_ofs = min(get1, h_end - 512 * 1024)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if h_ofs < 0:
 | 
					            if h_ofs < 0:
 | 
				
			||||||
                h_ofs = 0
 | 
					                h_ofs = 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            buf_ofs = (get2 - get1) - len(cdr)
 | 
					            buf_ofs = get1 - h_ofs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            dbg(
 | 
					            dbg(
 | 
				
			||||||
                "<cache> cdr {}, car {}-{}={} [-{}:]".format(
 | 
					                "<cache> cdr {}, car {}:{} |{}| [{}:]".format(
 | 
				
			||||||
                    len(cdr), h_ofs, h_end, h_end - h_ofs, buf_ofs
 | 
					                    len(cdr), h_ofs, h_end, h_end - h_ofs, buf_ofs
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            buf = self.gw.download_file_range(path, h_ofs, h_end - 1)
 | 
					            buf = self.gw.download_file_range(path, h_ofs, h_end)
 | 
				
			||||||
            ret = buf[-buf_ofs:] + cdr
 | 
					            if len(buf) == h_end - h_ofs:
 | 
				
			||||||
 | 
					                ret = buf[buf_ofs:] + cdr
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                ret = buf[get1 - h_ofs :]
 | 
				
			||||||
 | 
					                info(
 | 
				
			||||||
 | 
					                    "remote truncated {}:{} to |{}|, will return |{}|".format(
 | 
				
			||||||
 | 
					                        h_ofs, h_end, len(buf), len(ret)
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        elif car:
 | 
					        elif car:
 | 
				
			||||||
            h_ofs = get1 + len(car)
 | 
					            h_ofs = get1 + len(car)
 | 
				
			||||||
            h_end = h_ofs + 1024 * 1024
 | 
					            h_end = max(get2, h_ofs + 1024 * 1024)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if h_end > file_sz:
 | 
					            if h_end > file_sz:
 | 
				
			||||||
                h_end = file_sz
 | 
					                h_end = file_sz
 | 
				
			||||||
@@ -411,17 +648,22 @@ class CPPF(Operations):
 | 
				
			|||||||
            buf_ofs = (get2 - get1) - len(car)
 | 
					            buf_ofs = (get2 - get1) - len(car)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            dbg(
 | 
					            dbg(
 | 
				
			||||||
                "<cache> car {}, cdr {}-{}={} [:{}]".format(
 | 
					                "<cache> car {}, cdr {}:{} |{}| [:{}]".format(
 | 
				
			||||||
                    len(car), h_ofs, h_end, h_end - h_ofs, buf_ofs
 | 
					                    len(car), h_ofs, h_end, h_end - h_ofs, buf_ofs
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            buf = self.gw.download_file_range(path, h_ofs, h_end - 1)
 | 
					            buf = self.gw.download_file_range(path, h_ofs, h_end)
 | 
				
			||||||
            ret = car + buf[:buf_ofs]
 | 
					            ret = car + buf[:buf_ofs]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            h_ofs = get1 - 256 * 1024
 | 
					            if get2 - get1 <= 1024 * 1024:
 | 
				
			||||||
            h_end = get2 + 1024 * 1024
 | 
					                h_ofs = get1 - 256 * 1024
 | 
				
			||||||
 | 
					                h_end = get2 + 1024 * 1024
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                # big enough, doesn't need pads
 | 
				
			||||||
 | 
					                h_ofs = get1
 | 
				
			||||||
 | 
					                h_end = get2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if h_ofs < 0:
 | 
					            if h_ofs < 0:
 | 
				
			||||||
                h_ofs = 0
 | 
					                h_ofs = 0
 | 
				
			||||||
@@ -433,54 +675,99 @@ class CPPF(Operations):
 | 
				
			|||||||
            buf_end = buf_ofs + get2 - get1
 | 
					            buf_end = buf_ofs + get2 - get1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            dbg(
 | 
					            dbg(
 | 
				
			||||||
                "<cache> {}-{}={} [{}:{}]".format(
 | 
					                "<cache> {}:{} |{}| [{}:{}]".format(
 | 
				
			||||||
                    h_ofs, h_end, h_end - h_ofs, buf_ofs, buf_end
 | 
					                    h_ofs, h_end, h_end - h_ofs, buf_ofs, buf_end
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            buf = self.gw.download_file_range(path, h_ofs, h_end - 1)
 | 
					            buf = self.gw.download_file_range(path, h_ofs, h_end)
 | 
				
			||||||
            ret = buf[buf_ofs:buf_end]
 | 
					            ret = buf[buf_ofs:buf_end]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        cn = CacheNode([path, h_ofs], buf)
 | 
					        cn = CacheNode([path, h_ofs], buf)
 | 
				
			||||||
        # with self.filecache_mtx:
 | 
					        with self.filecache_mtx:
 | 
				
			||||||
        if True:
 | 
					            if len(self.filecache) >= self.n_filecache:
 | 
				
			||||||
            if len(self.filecache) > 6:
 | 
					 | 
				
			||||||
                self.filecache = self.filecache[1:] + [cn]
 | 
					                self.filecache = self.filecache[1:] + [cn]
 | 
				
			||||||
            else:
 | 
					            else:
 | 
				
			||||||
                self.filecache.append(cn)
 | 
					                self.filecache.append(cn)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return ret
 | 
					        return ret
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def readdir(self, path, fh=None):
 | 
					    def _readdir(self, path, fh=None):
 | 
				
			||||||
        path = path.strip("/")
 | 
					        path = path.strip("/")
 | 
				
			||||||
        log("readdir {}".format(path))
 | 
					        log("readdir [{}] [{}]".format(hexler(path), fh))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        ret = self.gw.listdir(path)
 | 
					        ret = self.gw.listdir(path)
 | 
				
			||||||
 | 
					        if not self.n_dircache:
 | 
				
			||||||
 | 
					            return ret
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # with self.dircache_mtx:
 | 
					        with self.dircache_mtx:
 | 
				
			||||||
        if True:
 | 
					 | 
				
			||||||
            cn = CacheNode(path, ret)
 | 
					            cn = CacheNode(path, ret)
 | 
				
			||||||
            self.dircache.append(cn)
 | 
					            self.dircache.append(cn)
 | 
				
			||||||
            self.clean_dircache()
 | 
					            self.clean_dircache()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return ret
 | 
					        return ret
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def readdir(self, path, fh=None):
 | 
				
			||||||
 | 
					        return [".", ".."] + self._readdir(path, fh)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def read(self, path, length, offset, fh=None):
 | 
					    def read(self, path, length, offset, fh=None):
 | 
				
			||||||
 | 
					        req_max = 1024 * 1024 * 8
 | 
				
			||||||
 | 
					        cache_max = 1024 * 1024 * 2
 | 
				
			||||||
 | 
					        if length > req_max:
 | 
				
			||||||
 | 
					            # windows actually doing 240 MiB read calls, sausage
 | 
				
			||||||
 | 
					            info("truncate |{}| to {}MiB".format(length, req_max >> 20))
 | 
				
			||||||
 | 
					            length = req_max
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        path = path.strip("/")
 | 
					        path = path.strip("/")
 | 
				
			||||||
 | 
					 | 
				
			||||||
        ofs2 = offset + length
 | 
					        ofs2 = offset + length
 | 
				
			||||||
        log("read {} @ {} len {} end {}".format(path, offset, length, ofs2))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        file_sz = self.getattr(path)["st_size"]
 | 
					        file_sz = self.getattr(path)["st_size"]
 | 
				
			||||||
        if ofs2 >= file_sz:
 | 
					        log(
 | 
				
			||||||
            ofs2 = file_sz - 1
 | 
					            "read {} |{}| {}:{} max {}".format(
 | 
				
			||||||
            log("truncate to len {} end {}".format((ofs2 - offset) + 1, ofs2))
 | 
					                hexler(path), length, offset, ofs2, file_sz
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        if ofs2 > file_sz:
 | 
				
			||||||
 | 
					            ofs2 = file_sz
 | 
				
			||||||
 | 
					            log("truncate to |{}| :{}".format(ofs2 - offset, ofs2))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # toggle cache here i suppose
 | 
					        if file_sz == 0 or offset >= ofs2:
 | 
				
			||||||
        # return self.get_cached_file(path, offset, ofs2, file_sz)
 | 
					            return b""
 | 
				
			||||||
        return self.gw.download_file_range(path, offset, ofs2 - 1)
 | 
					
 | 
				
			||||||
 | 
					        if self.n_filecache and length <= cache_max:
 | 
				
			||||||
 | 
					            ret = self.get_cached_file(path, offset, ofs2, file_sz)
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            ret = self.gw.download_file_range(path, offset, ofs2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return ret
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        fn = "cppf-{}-{}-{}".format(time.time(), offset, length)
 | 
				
			||||||
 | 
					        if False:
 | 
				
			||||||
 | 
					            with open(fn, "wb", len(ret)) as f:
 | 
				
			||||||
 | 
					                f.write(ret)
 | 
				
			||||||
 | 
					        elif self.n_filecache:
 | 
				
			||||||
 | 
					            ret2 = self.gw.download_file_range(path, offset, ofs2)
 | 
				
			||||||
 | 
					            if ret != ret2:
 | 
				
			||||||
 | 
					                info(fn)
 | 
				
			||||||
 | 
					                for v in [ret, ret2]:
 | 
				
			||||||
 | 
					                    try:
 | 
				
			||||||
 | 
					                        info(len(v))
 | 
				
			||||||
 | 
					                    except:
 | 
				
			||||||
 | 
					                        info("uhh " + repr(v))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                with open(fn + ".bad", "wb") as f:
 | 
				
			||||||
 | 
					                    f.write(ret)
 | 
				
			||||||
 | 
					                with open(fn + ".good", "wb") as f:
 | 
				
			||||||
 | 
					                    f.write(ret2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                raise Exception("cache bork")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return ret
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def getattr(self, path, fh=None):
 | 
					    def getattr(self, path, fh=None):
 | 
				
			||||||
 | 
					        log("getattr [{}]".format(hexler(path)))
 | 
				
			||||||
 | 
					        if WINDOWS:
 | 
				
			||||||
 | 
					            path = enwin(path)  # windows occasionally decodes f0xx to xx
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        path = path.strip("/")
 | 
					        path = path.strip("/")
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            dirpath, fname = path.rsplit("/", 1)
 | 
					            dirpath, fname = path.rsplit("/", 1)
 | 
				
			||||||
@@ -488,23 +775,34 @@ class CPPF(Operations):
 | 
				
			|||||||
            dirpath = ""
 | 
					            dirpath = ""
 | 
				
			||||||
            fname = path
 | 
					            fname = path
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        log("getattr {}".format(path))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if not path:
 | 
					        if not path:
 | 
				
			||||||
            return self.gw.stat_dir(time.time())
 | 
					            ret = self.gw.stat_dir(time.time())
 | 
				
			||||||
 | 
					            # dbg("=" + repr(ret))
 | 
				
			||||||
 | 
					            return ret
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        cn = self.get_cached_dir(dirpath)
 | 
					        cn = self.get_cached_dir(dirpath)
 | 
				
			||||||
        if cn:
 | 
					        if cn:
 | 
				
			||||||
            # log('cache ok')
 | 
					            log("cache ok")
 | 
				
			||||||
            dents = cn.data
 | 
					            dents = cn.data
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            log("cache miss")
 | 
					            dbg("cache miss")
 | 
				
			||||||
            dents = self.readdir(dirpath)
 | 
					            dents = self._readdir(dirpath)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for cache_name, cache_stat, _ in dents:
 | 
					        for cache_name, cache_stat, _ in dents:
 | 
				
			||||||
 | 
					            # if "qw" in cache_name and "qw" in fname:
 | 
				
			||||||
 | 
					            #     info(
 | 
				
			||||||
 | 
					            #         "cmp\n  [{}]\n  [{}]\n\n{}\n".format(
 | 
				
			||||||
 | 
					            #             hexler(cache_name),
 | 
				
			||||||
 | 
					            #             hexler(fname),
 | 
				
			||||||
 | 
					            #             "\n".join(traceback.format_stack()[:-1]),
 | 
				
			||||||
 | 
					            #         )
 | 
				
			||||||
 | 
					            #     )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if cache_name == fname:
 | 
					            if cache_name == fname:
 | 
				
			||||||
 | 
					                # dbg("=" + repr(cache_stat))
 | 
				
			||||||
                return cache_stat
 | 
					                return cache_stat
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        info("=ENOENT ({})".format(hexler(path)))
 | 
				
			||||||
        raise FuseOSError(errno.ENOENT)
 | 
					        raise FuseOSError(errno.ENOENT)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    access = None
 | 
					    access = None
 | 
				
			||||||
@@ -517,17 +815,178 @@ class CPPF(Operations):
 | 
				
			|||||||
    releasedir = None
 | 
					    releasedir = None
 | 
				
			||||||
    statfs = None
 | 
					    statfs = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if False:
 | 
				
			||||||
 | 
					        # incorrect semantics but good for debugging stuff like samba and msys2
 | 
				
			||||||
 | 
					        def access(self, path, mode):
 | 
				
			||||||
 | 
					            log("@@ access [{}] [{}]".format(path, mode))
 | 
				
			||||||
 | 
					            return 1 if self.getattr(path) else 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        def flush(self, path, fh):
 | 
				
			||||||
 | 
					            log("@@ flush [{}] [{}]".format(path, fh))
 | 
				
			||||||
 | 
					            return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        def getxattr(self, *args):
 | 
				
			||||||
 | 
					            log("@@ getxattr [{}]".format("] [".join(str(x) for x in args)))
 | 
				
			||||||
 | 
					            return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        def listxattr(self, *args):
 | 
				
			||||||
 | 
					            log("@@ listxattr [{}]".format("] [".join(str(x) for x in args)))
 | 
				
			||||||
 | 
					            return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        def open(self, path, flags):
 | 
				
			||||||
 | 
					            log("@@ open [{}] [{}]".format(path, flags))
 | 
				
			||||||
 | 
					            return 42
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        def opendir(self, fh):
 | 
				
			||||||
 | 
					            log("@@ opendir [{}]".format(fh))
 | 
				
			||||||
 | 
					            return 69
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        def release(self, ino, fi):
 | 
				
			||||||
 | 
					            log("@@ release [{}] [{}]".format(ino, fi))
 | 
				
			||||||
 | 
					            return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        def releasedir(self, ino, fi):
 | 
				
			||||||
 | 
					            log("@@ releasedir [{}] [{}]".format(ino, fi))
 | 
				
			||||||
 | 
					            return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        def statfs(self, path):
 | 
				
			||||||
 | 
					            log("@@ statfs [{}]".format(path))
 | 
				
			||||||
 | 
					            return {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if sys.platform == "win32":
 | 
				
			||||||
 | 
					        # quick compat for /mingw64/bin/python3 (msys2)
 | 
				
			||||||
 | 
					        def _open(self, path):
 | 
				
			||||||
 | 
					            try:
 | 
				
			||||||
 | 
					                x = self.getattr(path)
 | 
				
			||||||
 | 
					                if x["st_mode"] <= 0:
 | 
				
			||||||
 | 
					                    raise Exception()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                self.junk_fh_ctr += 1
 | 
				
			||||||
 | 
					                if self.junk_fh_ctr > 32000:  # TODO untested
 | 
				
			||||||
 | 
					                    self.junk_fh_ctr = 4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                return self.junk_fh_ctr
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            except Exception as ex:
 | 
				
			||||||
 | 
					                log("open ERR {}".format(repr(ex)))
 | 
				
			||||||
 | 
					                raise FuseOSError(errno.ENOENT)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        def open(self, path, flags):
 | 
				
			||||||
 | 
					            dbg("open [{}] [{}]".format(hexler(path), flags))
 | 
				
			||||||
 | 
					            return self._open(path)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        def opendir(self, path):
 | 
				
			||||||
 | 
					            dbg("opendir [{}]".format(hexler(path)))
 | 
				
			||||||
 | 
					            return self._open(path)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        def flush(self, path, fh):
 | 
				
			||||||
 | 
					            dbg("flush [{}] [{}]".format(hexler(path), fh))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        def release(self, ino, fi):
 | 
				
			||||||
 | 
					            dbg("release [{}] [{}]".format(hexler(ino), fi))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        def releasedir(self, ino, fi):
 | 
				
			||||||
 | 
					            dbg("releasedir [{}] [{}]".format(hexler(ino), fi))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        def access(self, path, mode):
 | 
				
			||||||
 | 
					            dbg("access [{}] [{}]".format(hexler(path), mode))
 | 
				
			||||||
 | 
					            try:
 | 
				
			||||||
 | 
					                x = self.getattr(path)
 | 
				
			||||||
 | 
					                if x["st_mode"] <= 0:
 | 
				
			||||||
 | 
					                    raise Exception()
 | 
				
			||||||
 | 
					            except:
 | 
				
			||||||
 | 
					                raise FuseOSError(errno.ENOENT)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TheArgparseFormatter(
 | 
				
			||||||
 | 
					    argparse.RawTextHelpFormatter, argparse.ArgumentDefaultsHelpFormatter
 | 
				
			||||||
 | 
					):
 | 
				
			||||||
 | 
					    pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def main():
 | 
					def main():
 | 
				
			||||||
    try:
 | 
					    global info, log, dbg
 | 
				
			||||||
        local, remote = sys.argv[1:]
 | 
					 | 
				
			||||||
    except:
 | 
					 | 
				
			||||||
        print("need arg 1: local directory")
 | 
					 | 
				
			||||||
        print("need arg 2: root url")
 | 
					 | 
				
			||||||
        return
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    FUSE(CPPF(remote), local, foreground=True, nothreads=True)
 | 
					    # filecache helps for reads that are ~64k or smaller;
 | 
				
			||||||
    # if nothreads=False also uncomment the `with *_mtx` things
 | 
					    #   linux generally does 128k so the cache is a slowdown,
 | 
				
			||||||
 | 
					    #   windows likes to use 4k and 64k so cache is required,
 | 
				
			||||||
 | 
					    #   value is numChunks (1~3M each) to keep in the cache
 | 
				
			||||||
 | 
					    nf = 24
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # dircache is always a boost,
 | 
				
			||||||
 | 
					    #   only want to disable it for tests etc,
 | 
				
			||||||
 | 
					    #   value is numSec until an entry goes stale
 | 
				
			||||||
 | 
					    nd = 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    where = "local directory"
 | 
				
			||||||
 | 
					    if WINDOWS:
 | 
				
			||||||
 | 
					        where += " or DRIVE:"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ex_pre = "\n  " + os.path.basename(__file__) + "  "
 | 
				
			||||||
 | 
					    examples = ["http://192.168.1.69:3923/music/  ./music"]
 | 
				
			||||||
 | 
					    if WINDOWS:
 | 
				
			||||||
 | 
					        examples.append("http://192.168.1.69:3923/music/  M:")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ap = argparse.ArgumentParser(
 | 
				
			||||||
 | 
					        formatter_class=TheArgparseFormatter,
 | 
				
			||||||
 | 
					        epilog="example:" + ex_pre + ex_pre.join(examples),
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    ap.add_argument(
 | 
				
			||||||
 | 
					        "-cd", metavar="NUM_SECONDS", type=float, default=nd, help="directory cache"
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    ap.add_argument(
 | 
				
			||||||
 | 
					        "-cf", metavar="NUM_BLOCKS", type=int, default=nf, help="file cache"
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    ap.add_argument("-a", metavar="PASSWORD", help="password")
 | 
				
			||||||
 | 
					    ap.add_argument("-d", action="store_true", help="enable debug")
 | 
				
			||||||
 | 
					    ap.add_argument("-te", metavar="PEM_FILE", help="certificate to expect/verify")
 | 
				
			||||||
 | 
					    ap.add_argument("-td", action="store_true", help="disable certificate check")
 | 
				
			||||||
 | 
					    ap.add_argument("base_url", type=str, help="remote copyparty URL to mount")
 | 
				
			||||||
 | 
					    ap.add_argument("local_path", type=str, help=where + " to mount it on")
 | 
				
			||||||
 | 
					    ar = ap.parse_args()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if ar.d:
 | 
				
			||||||
 | 
					        # windows terminals are slow (cmd.exe, mintty)
 | 
				
			||||||
 | 
					        # otoh fancy_log beats RecentLog on linux
 | 
				
			||||||
 | 
					        logger = RecentLog().put if WINDOWS else fancy_log
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        info = logger
 | 
				
			||||||
 | 
					        log = logger
 | 
				
			||||||
 | 
					        dbg = logger
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        # debug=off, speed is dontcare
 | 
				
			||||||
 | 
					        info = fancy_log
 | 
				
			||||||
 | 
					        log = null_log
 | 
				
			||||||
 | 
					        dbg = null_log
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if WINDOWS:
 | 
				
			||||||
 | 
					        os.system("")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for ch in '<>:"\\|?*':
 | 
				
			||||||
 | 
					            # microsoft maps illegal characters to f0xx
 | 
				
			||||||
 | 
					            # (e000 to f8ff is basic-plane private-use)
 | 
				
			||||||
 | 
					            bad_good[ch] = chr(ord(ch) + 0xF000)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for n in range(0, 0x100):
 | 
				
			||||||
 | 
					            # map surrogateescape to another private-use area
 | 
				
			||||||
 | 
					            bad_good[chr(n + 0xDC00)] = chr(n + 0xF100)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for k, v in bad_good.items():
 | 
				
			||||||
 | 
					            good_bad[v] = k
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    register_wtf8()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        with open("/etc/fuse.conf", "rb") as f:
 | 
				
			||||||
 | 
					            allow_other = b"\nuser_allow_other" in f.read()
 | 
				
			||||||
 | 
					    except:
 | 
				
			||||||
 | 
					        allow_other = WINDOWS or MACOS
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    args = {"foreground": True, "nothreads": True, "allow_other": allow_other}
 | 
				
			||||||
 | 
					    if not MACOS:
 | 
				
			||||||
 | 
					        args["nonempty"] = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    FUSE(CPPF(ar), ar.local_path, encoding="wtf-8", **args)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if __name__ == "__main__":
 | 
					if __name__ == "__main__":
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										590
									
								
								bin/copyparty-fuseb.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										590
									
								
								bin/copyparty-fuseb.py
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,590 @@
 | 
				
			|||||||
 | 
					#!/usr/bin/env python3
 | 
				
			||||||
 | 
					from __future__ import print_function, unicode_literals
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""copyparty-fuseb: remote copyparty as a local filesystem"""
 | 
				
			||||||
 | 
					__author__ = "ed <copyparty@ocv.me>"
 | 
				
			||||||
 | 
					__copyright__ = 2020
 | 
				
			||||||
 | 
					__license__ = "MIT"
 | 
				
			||||||
 | 
					__url__ = "https://github.com/9001/copyparty/"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import re
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
 | 
					import sys
 | 
				
			||||||
 | 
					import time
 | 
				
			||||||
 | 
					import stat
 | 
				
			||||||
 | 
					import errno
 | 
				
			||||||
 | 
					import struct
 | 
				
			||||||
 | 
					import threading
 | 
				
			||||||
 | 
					import http.client  # py2: httplib
 | 
				
			||||||
 | 
					import urllib.parse
 | 
				
			||||||
 | 
					from datetime import datetime
 | 
				
			||||||
 | 
					from urllib.parse import quote_from_bytes as quote
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					try:
 | 
				
			||||||
 | 
					    import fuse
 | 
				
			||||||
 | 
					    from fuse import Fuse
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fuse.fuse_python_api = (0, 2)
 | 
				
			||||||
 | 
					    if not hasattr(fuse, "__version__"):
 | 
				
			||||||
 | 
					        raise Exception("your fuse-python is way old")
 | 
				
			||||||
 | 
					except:
 | 
				
			||||||
 | 
					    print(
 | 
				
			||||||
 | 
					        "\n  could not import fuse; these may help:\n    python3 -m pip install --user fuse-python\n    apt install libfuse\n    modprobe fuse\n"
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    raise
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					mount a copyparty server (local or remote) as a filesystem
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					usage:
 | 
				
			||||||
 | 
					  python ./copyparty-fuseb.py -f -o allow_other,auto_unmount,nonempty,url=http://192.168.1.69:3923 /mnt/nas
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					dependencies:
 | 
				
			||||||
 | 
					  sudo apk add fuse-dev python3-dev
 | 
				
			||||||
 | 
					  python3 -m pip install --user fuse-python
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fork of copyparty-fuse.py based on fuse-python which
 | 
				
			||||||
 | 
					  appears to be more compliant than fusepy? since this works with samba
 | 
				
			||||||
 | 
					    (probably just my garbage code tbh)
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def threadless_log(msg):
 | 
				
			||||||
 | 
					    print(msg + "\n", end="")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def boring_log(msg):
 | 
				
			||||||
 | 
					    msg = "\033[36m{:012x}\033[0m {}\n".format(threading.current_thread().ident, msg)
 | 
				
			||||||
 | 
					    print(msg[4:], end="")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def rice_tid():
 | 
				
			||||||
 | 
					    tid = threading.current_thread().ident
 | 
				
			||||||
 | 
					    c = struct.unpack(b"B" * 5, struct.pack(b">Q", tid)[-5:])
 | 
				
			||||||
 | 
					    return "".join("\033[1;37;48;5;{}m{:02x}".format(x, x) for x in c) + "\033[0m"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def fancy_log(msg):
 | 
				
			||||||
 | 
					    print("{} {}\n".format(rice_tid(), msg), end="")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def null_log(msg):
 | 
				
			||||||
 | 
					    pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					info = fancy_log
 | 
				
			||||||
 | 
					log = fancy_log
 | 
				
			||||||
 | 
					dbg = fancy_log
 | 
				
			||||||
 | 
					log = null_log
 | 
				
			||||||
 | 
					dbg = null_log
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_tid():
 | 
				
			||||||
 | 
					    return threading.current_thread().ident
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def html_dec(txt):
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					        txt.replace("<", "<")
 | 
				
			||||||
 | 
					        .replace(">", ">")
 | 
				
			||||||
 | 
					        .replace(""", '"')
 | 
				
			||||||
 | 
					        .replace("&", "&")
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CacheNode(object):
 | 
				
			||||||
 | 
					    def __init__(self, tag, data):
 | 
				
			||||||
 | 
					        self.tag = tag
 | 
				
			||||||
 | 
					        self.data = data
 | 
				
			||||||
 | 
					        self.ts = time.time()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Stat(fuse.Stat):
 | 
				
			||||||
 | 
					    def __init__(self):
 | 
				
			||||||
 | 
					        self.st_mode = 0
 | 
				
			||||||
 | 
					        self.st_ino = 0
 | 
				
			||||||
 | 
					        self.st_dev = 0
 | 
				
			||||||
 | 
					        self.st_nlink = 1
 | 
				
			||||||
 | 
					        self.st_uid = 1000
 | 
				
			||||||
 | 
					        self.st_gid = 1000
 | 
				
			||||||
 | 
					        self.st_size = 0
 | 
				
			||||||
 | 
					        self.st_atime = 0
 | 
				
			||||||
 | 
					        self.st_mtime = 0
 | 
				
			||||||
 | 
					        self.st_ctime = 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Gateway(object):
 | 
				
			||||||
 | 
					    def __init__(self, base_url):
 | 
				
			||||||
 | 
					        self.base_url = base_url
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        ui = urllib.parse.urlparse(base_url)
 | 
				
			||||||
 | 
					        self.web_root = ui.path.strip("/")
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            self.web_host, self.web_port = ui.netloc.split(":")
 | 
				
			||||||
 | 
					            self.web_port = int(self.web_port)
 | 
				
			||||||
 | 
					        except:
 | 
				
			||||||
 | 
					            self.web_host = ui.netloc
 | 
				
			||||||
 | 
					            if ui.scheme == "http":
 | 
				
			||||||
 | 
					                self.web_port = 80
 | 
				
			||||||
 | 
					            elif ui.scheme == "https":
 | 
				
			||||||
 | 
					                raise Exception("todo")
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                raise Exception("bad url?")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.conns = {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def quotep(self, path):
 | 
				
			||||||
 | 
					        # TODO: mojibake support
 | 
				
			||||||
 | 
					        path = path.encode("utf-8", "ignore")
 | 
				
			||||||
 | 
					        return quote(path, safe="/")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def getconn(self, tid=None):
 | 
				
			||||||
 | 
					        tid = tid or get_tid()
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            return self.conns[tid]
 | 
				
			||||||
 | 
					        except:
 | 
				
			||||||
 | 
					            info("new conn [{}] [{}]".format(self.web_host, self.web_port))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            conn = http.client.HTTPConnection(self.web_host, self.web_port, timeout=260)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            self.conns[tid] = conn
 | 
				
			||||||
 | 
					            return conn
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def closeconn(self, tid=None):
 | 
				
			||||||
 | 
					        tid = tid or get_tid()
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            self.conns[tid].close()
 | 
				
			||||||
 | 
					            del self.conns[tid]
 | 
				
			||||||
 | 
					        except:
 | 
				
			||||||
 | 
					            pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def sendreq(self, *args, **kwargs):
 | 
				
			||||||
 | 
					        tid = get_tid()
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            c = self.getconn(tid)
 | 
				
			||||||
 | 
					            c.request(*list(args), **kwargs)
 | 
				
			||||||
 | 
					            return c.getresponse()
 | 
				
			||||||
 | 
					        except:
 | 
				
			||||||
 | 
					            self.closeconn(tid)
 | 
				
			||||||
 | 
					            c = self.getconn(tid)
 | 
				
			||||||
 | 
					            c.request(*list(args), **kwargs)
 | 
				
			||||||
 | 
					            return c.getresponse()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def listdir(self, path):
 | 
				
			||||||
 | 
					        web_path = self.quotep("/" + "/".join([self.web_root, path])) + "?dots"
 | 
				
			||||||
 | 
					        r = self.sendreq("GET", web_path)
 | 
				
			||||||
 | 
					        if r.status != 200:
 | 
				
			||||||
 | 
					            self.closeconn()
 | 
				
			||||||
 | 
					            raise Exception(
 | 
				
			||||||
 | 
					                "http error {} reading dir {} in {}".format(
 | 
				
			||||||
 | 
					                    r.status, web_path, rice_tid()
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return self.parse_html(r)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def download_file_range(self, path, ofs1, ofs2):
 | 
				
			||||||
 | 
					        web_path = self.quotep("/" + "/".join([self.web_root, path])) + "?raw"
 | 
				
			||||||
 | 
					        hdr_range = "bytes={}-{}".format(ofs1, ofs2 - 1)
 | 
				
			||||||
 | 
					        log("downloading {}".format(hdr_range))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        r = self.sendreq("GET", web_path, headers={"Range": hdr_range})
 | 
				
			||||||
 | 
					        if r.status != http.client.PARTIAL_CONTENT:
 | 
				
			||||||
 | 
					            self.closeconn()
 | 
				
			||||||
 | 
					            raise Exception(
 | 
				
			||||||
 | 
					                "http error {} reading file {} range {} in {}".format(
 | 
				
			||||||
 | 
					                    r.status, web_path, hdr_range, rice_tid()
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return r.read()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def parse_html(self, datasrc):
 | 
				
			||||||
 | 
					        ret = []
 | 
				
			||||||
 | 
					        remainder = b""
 | 
				
			||||||
 | 
					        ptn = re.compile(
 | 
				
			||||||
 | 
					            r"^<tr><td>(-|DIR)</td><td><a [^>]+>([^<]+)</a></td><td>([^<]+)</td><td>([^<]+)</td></tr>$"
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        while True:
 | 
				
			||||||
 | 
					            buf = remainder + datasrc.read(4096)
 | 
				
			||||||
 | 
					            # print('[{}]'.format(buf.decode('utf-8')))
 | 
				
			||||||
 | 
					            if not buf:
 | 
				
			||||||
 | 
					                break
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            remainder = b""
 | 
				
			||||||
 | 
					            endpos = buf.rfind(b"\n")
 | 
				
			||||||
 | 
					            if endpos >= 0:
 | 
				
			||||||
 | 
					                remainder = buf[endpos + 1 :]
 | 
				
			||||||
 | 
					                buf = buf[:endpos]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            lines = buf.decode("utf-8").split("\n")
 | 
				
			||||||
 | 
					            for line in lines:
 | 
				
			||||||
 | 
					                m = ptn.match(line)
 | 
				
			||||||
 | 
					                if not m:
 | 
				
			||||||
 | 
					                    # print(line)
 | 
				
			||||||
 | 
					                    continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                ftype, fname, fsize, fdate = m.groups()
 | 
				
			||||||
 | 
					                fname = html_dec(fname)
 | 
				
			||||||
 | 
					                ts = datetime.strptime(fdate, "%Y-%m-%d %H:%M:%S").timestamp()
 | 
				
			||||||
 | 
					                sz = int(fsize)
 | 
				
			||||||
 | 
					                if ftype == "-":
 | 
				
			||||||
 | 
					                    ret.append([fname, self.stat_file(ts, sz), 0])
 | 
				
			||||||
 | 
					                else:
 | 
				
			||||||
 | 
					                    ret.append([fname, self.stat_dir(ts, sz), 0])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return ret
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def stat_dir(self, ts, sz=4096):
 | 
				
			||||||
 | 
					        ret = Stat()
 | 
				
			||||||
 | 
					        ret.st_mode = stat.S_IFDIR | 0o555
 | 
				
			||||||
 | 
					        ret.st_nlink = 2
 | 
				
			||||||
 | 
					        ret.st_size = sz
 | 
				
			||||||
 | 
					        ret.st_atime = ts
 | 
				
			||||||
 | 
					        ret.st_mtime = ts
 | 
				
			||||||
 | 
					        ret.st_ctime = ts
 | 
				
			||||||
 | 
					        return ret
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def stat_file(self, ts, sz):
 | 
				
			||||||
 | 
					        ret = Stat()
 | 
				
			||||||
 | 
					        ret.st_mode = stat.S_IFREG | 0o444
 | 
				
			||||||
 | 
					        ret.st_size = sz
 | 
				
			||||||
 | 
					        ret.st_atime = ts
 | 
				
			||||||
 | 
					        ret.st_mtime = ts
 | 
				
			||||||
 | 
					        ret.st_ctime = ts
 | 
				
			||||||
 | 
					        return ret
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CPPF(Fuse):
 | 
				
			||||||
 | 
					    def __init__(self, *args, **kwargs):
 | 
				
			||||||
 | 
					        Fuse.__init__(self, *args, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.url = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.dircache = []
 | 
				
			||||||
 | 
					        self.dircache_mtx = threading.Lock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.filecache = []
 | 
				
			||||||
 | 
					        self.filecache_mtx = threading.Lock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def init2(self):
 | 
				
			||||||
 | 
					        # TODO figure out how python-fuse wanted this to go
 | 
				
			||||||
 | 
					        self.gw = Gateway(self.url)  # .decode('utf-8'))
 | 
				
			||||||
 | 
					        info("up")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def clean_dircache(self):
 | 
				
			||||||
 | 
					        """not threadsafe"""
 | 
				
			||||||
 | 
					        now = time.time()
 | 
				
			||||||
 | 
					        cutoff = 0
 | 
				
			||||||
 | 
					        for cn in self.dircache:
 | 
				
			||||||
 | 
					            if now - cn.ts > 1:
 | 
				
			||||||
 | 
					                cutoff += 1
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                break
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if cutoff > 0:
 | 
				
			||||||
 | 
					            self.dircache = self.dircache[cutoff:]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_cached_dir(self, dirpath):
 | 
				
			||||||
 | 
					        # with self.dircache_mtx:
 | 
				
			||||||
 | 
					        if True:
 | 
				
			||||||
 | 
					            self.clean_dircache()
 | 
				
			||||||
 | 
					            for cn in self.dircache:
 | 
				
			||||||
 | 
					                if cn.tag == dirpath:
 | 
				
			||||||
 | 
					                    return cn
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					            ,-------------------------------,  g1>=c1, g2<=c2
 | 
				
			||||||
 | 
					            |cache1                   cache2|  buf[g1-c1:(g1-c1)+(g2-g1)]
 | 
				
			||||||
 | 
					            `-------------------------------'
 | 
				
			||||||
 | 
					                    ,---------------,
 | 
				
			||||||
 | 
					                    |get1       get2|
 | 
				
			||||||
 | 
					                    `---------------'
 | 
				
			||||||
 | 
					    __________________________________________________________________________
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            ,-------------------------------,  g2<=c2, (g2>=c1)
 | 
				
			||||||
 | 
					            |cache1                   cache2|  cdr=buf[:g2-c1]
 | 
				
			||||||
 | 
					            `-------------------------------'  dl car; g1-512K:c1
 | 
				
			||||||
 | 
					    ,---------------,
 | 
				
			||||||
 | 
					    |get1       get2|
 | 
				
			||||||
 | 
					    `---------------'
 | 
				
			||||||
 | 
					    __________________________________________________________________________
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            ,-------------------------------,  g1>=c1, (g1<=c2)
 | 
				
			||||||
 | 
					            |cache1                   cache2|  car=buf[c2-g1:]
 | 
				
			||||||
 | 
					            `-------------------------------'  dl cdr; c2:c2+1M
 | 
				
			||||||
 | 
					                                    ,---------------,
 | 
				
			||||||
 | 
					                                    |get1       get2|
 | 
				
			||||||
 | 
					                                    `---------------'
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_cached_file(self, path, get1, get2, file_sz):
 | 
				
			||||||
 | 
					        car = None
 | 
				
			||||||
 | 
					        cdr = None
 | 
				
			||||||
 | 
					        ncn = -1
 | 
				
			||||||
 | 
					        # with self.filecache_mtx:
 | 
				
			||||||
 | 
					        if True:
 | 
				
			||||||
 | 
					            dbg("cache request from {} to {}, size {}".format(get1, get2, file_sz))
 | 
				
			||||||
 | 
					            for cn in self.filecache:
 | 
				
			||||||
 | 
					                ncn += 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                cache_path, cache1 = cn.tag
 | 
				
			||||||
 | 
					                if cache_path != path:
 | 
				
			||||||
 | 
					                    continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                cache2 = cache1 + len(cn.data)
 | 
				
			||||||
 | 
					                if get2 <= cache1 or get1 >= cache2:
 | 
				
			||||||
 | 
					                    continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if get1 >= cache1 and get2 <= cache2:
 | 
				
			||||||
 | 
					                    # keep cache entry alive by moving it to the end
 | 
				
			||||||
 | 
					                    self.filecache = (
 | 
				
			||||||
 | 
					                        self.filecache[:ncn] + self.filecache[ncn + 1 :] + [cn]
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                    buf_ofs = get1 - cache1
 | 
				
			||||||
 | 
					                    buf_end = buf_ofs + (get2 - get1)
 | 
				
			||||||
 | 
					                    dbg(
 | 
				
			||||||
 | 
					                        "found all ({}, {} to {}, len {}) [{}:{}] = {}".format(
 | 
				
			||||||
 | 
					                            ncn,
 | 
				
			||||||
 | 
					                            cache1,
 | 
				
			||||||
 | 
					                            cache2,
 | 
				
			||||||
 | 
					                            len(cn.data),
 | 
				
			||||||
 | 
					                            buf_ofs,
 | 
				
			||||||
 | 
					                            buf_end,
 | 
				
			||||||
 | 
					                            buf_end - buf_ofs,
 | 
				
			||||||
 | 
					                        )
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                    return cn.data[buf_ofs:buf_end]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if get2 < cache2:
 | 
				
			||||||
 | 
					                    x = cn.data[: get2 - cache1]
 | 
				
			||||||
 | 
					                    if not cdr or len(cdr) < len(x):
 | 
				
			||||||
 | 
					                        dbg(
 | 
				
			||||||
 | 
					                            "found car ({}, {} to {}, len {}) [:{}-{}] = [:{}] = {}".format(
 | 
				
			||||||
 | 
					                                ncn,
 | 
				
			||||||
 | 
					                                cache1,
 | 
				
			||||||
 | 
					                                cache2,
 | 
				
			||||||
 | 
					                                len(cn.data),
 | 
				
			||||||
 | 
					                                get2,
 | 
				
			||||||
 | 
					                                cache1,
 | 
				
			||||||
 | 
					                                get2 - cache1,
 | 
				
			||||||
 | 
					                                len(x),
 | 
				
			||||||
 | 
					                            )
 | 
				
			||||||
 | 
					                        )
 | 
				
			||||||
 | 
					                        cdr = x
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if get1 > cache1:
 | 
				
			||||||
 | 
					                    x = cn.data[-(cache2 - get1) :]
 | 
				
			||||||
 | 
					                    if not car or len(car) < len(x):
 | 
				
			||||||
 | 
					                        dbg(
 | 
				
			||||||
 | 
					                            "found cdr ({}, {} to {}, len {}) [-({}-{}):] = [-{}:] = {}".format(
 | 
				
			||||||
 | 
					                                ncn,
 | 
				
			||||||
 | 
					                                cache1,
 | 
				
			||||||
 | 
					                                cache2,
 | 
				
			||||||
 | 
					                                len(cn.data),
 | 
				
			||||||
 | 
					                                cache2,
 | 
				
			||||||
 | 
					                                get1,
 | 
				
			||||||
 | 
					                                cache2 - get1,
 | 
				
			||||||
 | 
					                                len(x),
 | 
				
			||||||
 | 
					                            )
 | 
				
			||||||
 | 
					                        )
 | 
				
			||||||
 | 
					                        car = x
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                raise Exception("what")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if car and cdr:
 | 
				
			||||||
 | 
					            dbg("<cache> have both")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            ret = car + cdr
 | 
				
			||||||
 | 
					            if len(ret) == get2 - get1:
 | 
				
			||||||
 | 
					                return ret
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            raise Exception("{} + {} != {} - {}".format(len(car), len(cdr), get2, get1))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        elif cdr:
 | 
				
			||||||
 | 
					            h_end = get1 + (get2 - get1) - len(cdr)
 | 
				
			||||||
 | 
					            h_ofs = h_end - 512 * 1024
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if h_ofs < 0:
 | 
				
			||||||
 | 
					                h_ofs = 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            buf_ofs = (get2 - get1) - len(cdr)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            dbg(
 | 
				
			||||||
 | 
					                "<cache> cdr {}, car {}-{}={} [-{}:]".format(
 | 
				
			||||||
 | 
					                    len(cdr), h_ofs, h_end, h_end - h_ofs, buf_ofs
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            buf = self.gw.download_file_range(path, h_ofs, h_end)
 | 
				
			||||||
 | 
					            ret = buf[-buf_ofs:] + cdr
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        elif car:
 | 
				
			||||||
 | 
					            h_ofs = get1 + len(car)
 | 
				
			||||||
 | 
					            h_end = h_ofs + 1024 * 1024
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if h_end > file_sz:
 | 
				
			||||||
 | 
					                h_end = file_sz
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            buf_ofs = (get2 - get1) - len(car)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            dbg(
 | 
				
			||||||
 | 
					                "<cache> car {}, cdr {}-{}={} [:{}]".format(
 | 
				
			||||||
 | 
					                    len(car), h_ofs, h_end, h_end - h_ofs, buf_ofs
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            buf = self.gw.download_file_range(path, h_ofs, h_end)
 | 
				
			||||||
 | 
					            ret = car + buf[:buf_ofs]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            h_ofs = get1 - 256 * 1024
 | 
				
			||||||
 | 
					            h_end = get2 + 1024 * 1024
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if h_ofs < 0:
 | 
				
			||||||
 | 
					                h_ofs = 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if h_end > file_sz:
 | 
				
			||||||
 | 
					                h_end = file_sz
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            buf_ofs = get1 - h_ofs
 | 
				
			||||||
 | 
					            buf_end = buf_ofs + get2 - get1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            dbg(
 | 
				
			||||||
 | 
					                "<cache> {}-{}={} [{}:{}]".format(
 | 
				
			||||||
 | 
					                    h_ofs, h_end, h_end - h_ofs, buf_ofs, buf_end
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            buf = self.gw.download_file_range(path, h_ofs, h_end)
 | 
				
			||||||
 | 
					            ret = buf[buf_ofs:buf_end]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        cn = CacheNode([path, h_ofs], buf)
 | 
				
			||||||
 | 
					        # with self.filecache_mtx:
 | 
				
			||||||
 | 
					        if True:
 | 
				
			||||||
 | 
					            if len(self.filecache) > 6:
 | 
				
			||||||
 | 
					                self.filecache = self.filecache[1:] + [cn]
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                self.filecache.append(cn)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return ret
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _readdir(self, path):
 | 
				
			||||||
 | 
					        path = path.strip("/")
 | 
				
			||||||
 | 
					        log("readdir {}".format(path))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        ret = self.gw.listdir(path)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # with self.dircache_mtx:
 | 
				
			||||||
 | 
					        if True:
 | 
				
			||||||
 | 
					            cn = CacheNode(path, ret)
 | 
				
			||||||
 | 
					            self.dircache.append(cn)
 | 
				
			||||||
 | 
					            self.clean_dircache()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return ret
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def readdir(self, path, offset):
 | 
				
			||||||
 | 
					        for e in self._readdir(path)[offset:]:
 | 
				
			||||||
 | 
					            # log("yield [{}]".format(e[0]))
 | 
				
			||||||
 | 
					            yield fuse.Direntry(e[0])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def open(self, path, flags):
 | 
				
			||||||
 | 
					        if (flags & (os.O_RDONLY | os.O_WRONLY | os.O_RDWR)) != os.O_RDONLY:
 | 
				
			||||||
 | 
					            return -errno.EACCES
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        st = self.getattr(path)
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            if st.st_nlink > 0:
 | 
				
			||||||
 | 
					                return st
 | 
				
			||||||
 | 
					        except:
 | 
				
			||||||
 | 
					            return st  # -int(os.errcode)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def read(self, path, length, offset, fh=None, *args):
 | 
				
			||||||
 | 
					        if args:
 | 
				
			||||||
 | 
					            log("unexpected args [" + "] [".join(repr(x) for x in args) + "]")
 | 
				
			||||||
 | 
					            raise Exception()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        path = path.strip("/")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        ofs2 = offset + length
 | 
				
			||||||
 | 
					        log("read {} @ {} len {} end {}".format(path, offset, length, ofs2))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        st = self.getattr(path)
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            file_sz = st.st_size
 | 
				
			||||||
 | 
					        except:
 | 
				
			||||||
 | 
					            return st  # -int(os.errcode)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if ofs2 > file_sz:
 | 
				
			||||||
 | 
					            ofs2 = file_sz
 | 
				
			||||||
 | 
					            log("truncate to len {} end {}".format(ofs2 - offset, ofs2))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if file_sz == 0 or offset >= ofs2:
 | 
				
			||||||
 | 
					            return b""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # toggle cache here i suppose
 | 
				
			||||||
 | 
					        # return self.get_cached_file(path, offset, ofs2, file_sz)
 | 
				
			||||||
 | 
					        return self.gw.download_file_range(path, offset, ofs2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def getattr(self, path):
 | 
				
			||||||
 | 
					        log("getattr [{}]".format(path))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        path = path.strip("/")
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            dirpath, fname = path.rsplit("/", 1)
 | 
				
			||||||
 | 
					        except:
 | 
				
			||||||
 | 
					            dirpath = ""
 | 
				
			||||||
 | 
					            fname = path
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if not path:
 | 
				
			||||||
 | 
					            ret = self.gw.stat_dir(time.time())
 | 
				
			||||||
 | 
					            dbg("=root")
 | 
				
			||||||
 | 
					            return ret
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        cn = self.get_cached_dir(dirpath)
 | 
				
			||||||
 | 
					        if cn:
 | 
				
			||||||
 | 
					            log("cache ok")
 | 
				
			||||||
 | 
					            dents = cn.data
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            log("cache miss")
 | 
				
			||||||
 | 
					            dents = self._readdir(dirpath)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for cache_name, cache_stat, _ in dents:
 | 
				
			||||||
 | 
					            if cache_name == fname:
 | 
				
			||||||
 | 
					                dbg("=file")
 | 
				
			||||||
 | 
					                return cache_stat
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        log("=404")
 | 
				
			||||||
 | 
					        return -errno.ENOENT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def main():
 | 
				
			||||||
 | 
					    server = CPPF()
 | 
				
			||||||
 | 
					    server.parser.add_option(mountopt="url", metavar="BASE_URL", default=None)
 | 
				
			||||||
 | 
					    server.parse(values=server, errex=1)
 | 
				
			||||||
 | 
					    if not server.url or not str(server.url).startswith("http"):
 | 
				
			||||||
 | 
					        print("\nerror:")
 | 
				
			||||||
 | 
					        print("  need argument: -o url=<...>")
 | 
				
			||||||
 | 
					        print("  need argument: mount-path")
 | 
				
			||||||
 | 
					        print("example:")
 | 
				
			||||||
 | 
					        print(
 | 
				
			||||||
 | 
					            "  ./copyparty-fuseb.py -f -o allow_other,auto_unmount,nonempty,url=http://192.168.1.69:3923 /mnt/nas"
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        sys.exit(1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    server.init2()
 | 
				
			||||||
 | 
					    threading.Thread(target=server.main, daemon=True).start()
 | 
				
			||||||
 | 
					    while True:
 | 
				
			||||||
 | 
					        time.sleep(9001)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if __name__ == "__main__":
 | 
				
			||||||
 | 
					    main()
 | 
				
			||||||
@@ -118,7 +118,7 @@ printf ']}' >> /dev/shm/$salt.hs
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
printf '\033[36m'
 | 
					printf '\033[36m'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#curl "http://$target:1234$posturl/handshake.php" -H "Content-Type: text/plain;charset=UTF-8" -H "Cookie: cppwd=$passwd" --data "$(cat "/dev/shm/$salt.hs")" | tee /dev/shm/$salt.res
 | 
					#curl "http://$target:3923$posturl/handshake.php" -H "Content-Type: text/plain;charset=UTF-8" -H "Cookie: cppwd=$passwd" --data "$(cat "/dev/shm/$salt.hs")" | tee /dev/shm/$salt.res
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
@@ -135,7 +135,7 @@ EOF
 | 
				
			|||||||
    cat /dev/shm/$salt.hs
 | 
					    cat /dev/shm/$salt.hs
 | 
				
			||||||
} |
 | 
					} |
 | 
				
			||||||
tee /dev/shm/$salt.hsb |
 | 
					tee /dev/shm/$salt.hsb |
 | 
				
			||||||
ncat $target 1234 |
 | 
					ncat $target 3923 |
 | 
				
			||||||
tee /dev/shm/$salt.hs1r
 | 
					tee /dev/shm/$salt.hs1r
 | 
				
			||||||
 | 
					
 | 
				
			||||||
wark="$(cat /dev/shm/$salt.hs1r | getwark)"
 | 
					wark="$(cat /dev/shm/$salt.hs1r | getwark)"
 | 
				
			||||||
@@ -190,7 +190,7 @@ EOF
 | 
				
			|||||||
    nchunk=$((nchunk+1))
 | 
					    nchunk=$((nchunk+1))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
done |
 | 
					done |
 | 
				
			||||||
ncat $target 1234 |
 | 
					ncat $target 3923 |
 | 
				
			||||||
tee /dev/shm/$salt.pr
 | 
					tee /dev/shm/$salt.pr
 | 
				
			||||||
 | 
					
 | 
				
			||||||
t=$(date +%s.%N)
 | 
					t=$(date +%s.%N)
 | 
				
			||||||
@@ -201,7 +201,7 @@ t=$(date +%s.%N)
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
printf '\033[36m'
 | 
					printf '\033[36m'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ncat $target 1234 < /dev/shm/$salt.hsb |
 | 
					ncat $target 3923 < /dev/shm/$salt.hsb |
 | 
				
			||||||
tee /dev/shm/$salt.hs2r |
 | 
					tee /dev/shm/$salt.hs2r |
 | 
				
			||||||
grep -E '"hash": ?\[ *\]'
 | 
					grep -E '"hash": ?\[ *\]'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										15
									
								
								contrib/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								contrib/README.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					### [`copyparty.bat`](copyparty.bat)
 | 
				
			||||||
 | 
					* launches copyparty with no arguments (anon read+write within same folder)
 | 
				
			||||||
 | 
					* intended for windows machines with no python.exe in PATH
 | 
				
			||||||
 | 
					* works on windows, linux and macos
 | 
				
			||||||
 | 
					* assumes `copyparty-sfx.py` was renamed to `copyparty.py` in the same folder as `copyparty.bat`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### [`index.html`](index.html)
 | 
				
			||||||
 | 
					* drop-in redirect from an httpd to copyparty
 | 
				
			||||||
 | 
					* assumes the webserver and copyparty is running on the same server/IP
 | 
				
			||||||
 | 
					* modify `10.13.1.1` as necessary if you wish to support browsers without javascript
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# OS integration
 | 
				
			||||||
 | 
					init-scripts to start copyparty as a service
 | 
				
			||||||
 | 
					* [`systemd/copyparty.service`](systemd/copyparty.service)
 | 
				
			||||||
 | 
					* [`openrc/copyparty`](openrc/copyparty)
 | 
				
			||||||
							
								
								
									
										33
									
								
								contrib/copyparty.bat
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								contrib/copyparty.bat
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,33 @@
 | 
				
			|||||||
 | 
					exec python "$(dirname "$0")"/copyparty.py
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@rem on linux, the above will execute and the script will terminate
 | 
				
			||||||
 | 
					@rem on windows, the rest of this script will run
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@echo off
 | 
				
			||||||
 | 
					cls
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					set py=
 | 
				
			||||||
 | 
					for /f %%i in ('where python 2^>nul') do (
 | 
				
			||||||
 | 
					    set "py=%%i"
 | 
				
			||||||
 | 
					    goto c1
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					:c1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if [%py%] == [] (
 | 
				
			||||||
 | 
					    for /f %%i in ('where /r "%localappdata%\programs\python" python 2^>nul') do (
 | 
				
			||||||
 | 
					        set "py=%%i"
 | 
				
			||||||
 | 
					        goto c2
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					:c2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if [%py%] == [] set "py=c:\python27\python.exe"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if not exist "%py%" (
 | 
				
			||||||
 | 
					    echo could not find python
 | 
				
			||||||
 | 
					    echo(
 | 
				
			||||||
 | 
					    pause
 | 
				
			||||||
 | 
					    exit /b
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					start cmd /c %py% "%~dp0\copyparty.py"
 | 
				
			||||||
							
								
								
									
										43
									
								
								contrib/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								contrib/index.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,43 @@
 | 
				
			|||||||
 | 
					<!DOCTYPE html>
 | 
				
			||||||
 | 
					<html lang="en">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<head>
 | 
				
			||||||
 | 
						<meta charset="utf-8">
 | 
				
			||||||
 | 
						<title>⇆🎉 redirect</title>
 | 
				
			||||||
 | 
						<meta http-equiv="X-UA-Compatible" content="IE=edge">
 | 
				
			||||||
 | 
						<style>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					html, body {
 | 
				
			||||||
 | 
						font-family: sans-serif;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					body {
 | 
				
			||||||
 | 
						padding: 1em 2em;
 | 
				
			||||||
 | 
						font-size: 1.5em;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					a {
 | 
				
			||||||
 | 
						font-size: 1.2em;
 | 
				
			||||||
 | 
						padding: .1em;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
 | 
					</head>
 | 
				
			||||||
 | 
					<body>
 | 
				
			||||||
 | 
						<span id="desc">you probably want</span> <a id="redir" href="//10.13.1.1:3923/">copyparty</a>
 | 
				
			||||||
 | 
						<script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var a = document.getElementById('redir'),
 | 
				
			||||||
 | 
						proto = window.location.protocol.indexOf('https') === 0 ? 'https' : 'http',
 | 
				
			||||||
 | 
						loc = window.location.hostname || '127.0.0.1',
 | 
				
			||||||
 | 
						port = a.getAttribute('href').split(':').pop().split('/')[0],
 | 
				
			||||||
 | 
						url = proto + '://' + loc + ':' + port + '/';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					a.setAttribute('href', url);
 | 
				
			||||||
 | 
					document.getElementById('desc').innerHTML = 'redirecting to';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					setTimeout(function() {
 | 
				
			||||||
 | 
						window.location.href = url;
 | 
				
			||||||
 | 
					}, 500);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					</body>
 | 
				
			||||||
 | 
					</html>
 | 
				
			||||||
							
								
								
									
										18
									
								
								contrib/openrc/copyparty
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								contrib/openrc/copyparty
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					#!/sbin/openrc-run
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# this will start `/usr/local/bin/copyparty-sfx.py`
 | 
				
			||||||
 | 
					# and share '/mnt' with anonymous read+write
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# installation:
 | 
				
			||||||
 | 
					#   cp -pv copyparty /etc/init.d && rc-update add copyparty
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# you may want to:
 | 
				
			||||||
 | 
					#   change '/usr/bin/python' to another interpreter
 | 
				
			||||||
 | 
					#   change '/mnt::a' to another location or permission-set
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					name="$SVCNAME"
 | 
				
			||||||
 | 
					command_background=true
 | 
				
			||||||
 | 
					pidfile="/var/run/$SVCNAME.pid"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					command="/usr/bin/python /usr/local/bin/copyparty-sfx.py"
 | 
				
			||||||
 | 
					command_args="-q -v /mnt::a"
 | 
				
			||||||
							
								
								
									
										19
									
								
								contrib/systemd/copyparty.service
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								contrib/systemd/copyparty.service
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					# this will start `/usr/local/bin/copyparty-sfx.py`
 | 
				
			||||||
 | 
					# and share '/mnt' with anonymous read+write
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# installation:
 | 
				
			||||||
 | 
					#   cp -pv copyparty.service /etc/systemd/system && systemctl enable --now copyparty
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# you may want to:
 | 
				
			||||||
 | 
					#   change '/usr/bin/python' to another interpreter
 | 
				
			||||||
 | 
					#   change '/mnt::a' to another location or permission-set
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[Unit]
 | 
				
			||||||
 | 
					Description=copyparty file server
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[Service]
 | 
				
			||||||
 | 
					ExecStart=/usr/bin/python /usr/local/bin/copyparty-sfx.py -q -v /mnt::a
 | 
				
			||||||
 | 
					ExecStartPre=/bin/bash -c 'mkdir -p /run/tmpfiles.d/ && echo "x /tmp/pe-copyparty*" > /run/tmpfiles.d/copyparty.conf'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[Install]
 | 
				
			||||||
 | 
					WantedBy=multi-user.target
 | 
				
			||||||
@@ -16,6 +16,8 @@ if platform.system() == "Windows":
 | 
				
			|||||||
VT100 = not WINDOWS or WINDOWS >= [10, 0, 14393]
 | 
					VT100 = not WINDOWS or WINDOWS >= [10, 0, 14393]
 | 
				
			||||||
# introduced in anniversary update
 | 
					# introduced in anniversary update
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					MACOS = platform.system() == "Darwin"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class EnvParams(object):
 | 
					class EnvParams(object):
 | 
				
			||||||
    def __init__(self):
 | 
					    def __init__(self):
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -127,13 +127,18 @@ def main():
 | 
				
			|||||||
        "-c", metavar="PATH", type=str, action="append", help="add config file"
 | 
					        "-c", metavar="PATH", type=str, action="append", help="add config file"
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    ap.add_argument("-i", metavar="IP", type=str, default="0.0.0.0", help="ip to bind")
 | 
					    ap.add_argument("-i", metavar="IP", type=str, default="0.0.0.0", help="ip to bind")
 | 
				
			||||||
    ap.add_argument("-p", metavar="PORT", type=int, default=1234, help="port to bind")
 | 
					    ap.add_argument("-p", metavar="PORT", type=int, default=3923, help="port to bind")
 | 
				
			||||||
    ap.add_argument("-nc", metavar="NUM", type=int, default=16, help="max num clients")
 | 
					    ap.add_argument("-nc", metavar="NUM", type=int, default=64, help="max num clients")
 | 
				
			||||||
    ap.add_argument("-j", metavar="CORES", type=int, help="max num cpu cores")
 | 
					    ap.add_argument(
 | 
				
			||||||
 | 
					        "-j", metavar="CORES", type=int, default=1, help="max num cpu cores"
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
    ap.add_argument("-a", metavar="ACCT", type=str, action="append", help="add account")
 | 
					    ap.add_argument("-a", metavar="ACCT", type=str, action="append", help="add account")
 | 
				
			||||||
    ap.add_argument("-v", metavar="VOL", type=str, action="append", help="add volume")
 | 
					    ap.add_argument("-v", metavar="VOL", type=str, action="append", help="add volume")
 | 
				
			||||||
    ap.add_argument("-q", action="store_true", help="quiet")
 | 
					    ap.add_argument("-q", action="store_true", help="quiet")
 | 
				
			||||||
    ap.add_argument("-nw", action="store_true", help="benchmark: disable writing")
 | 
					    ap.add_argument("-ed", action="store_true", help="enable ?dots")
 | 
				
			||||||
 | 
					    ap.add_argument("-nw", action="store_true", help="disable writes (benchmark)")
 | 
				
			||||||
 | 
					    ap.add_argument("-nih", action="store_true", help="no info hostname")
 | 
				
			||||||
 | 
					    ap.add_argument("-nid", action="store_true", help="no info disk-usage")
 | 
				
			||||||
    al = ap.parse_args()
 | 
					    al = ap.parse_args()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    SvcHub(al).run()
 | 
					    SvcHub(al).run()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,8 +1,8 @@
 | 
				
			|||||||
# coding: utf-8
 | 
					# coding: utf-8
 | 
				
			||||||
 | 
					
 | 
				
			||||||
VERSION = (0, 3, 0)
 | 
					VERSION = (0, 5, 3)
 | 
				
			||||||
CODENAME = "docuparty"
 | 
					CODENAME = "fuse jelly"
 | 
				
			||||||
BUILD_DT = (2020, 5, 6)
 | 
					BUILD_DT = (2020, 11, 13)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
S_VERSION = ".".join(map(str, VERSION))
 | 
					S_VERSION = ".".join(map(str, VERSION))
 | 
				
			||||||
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)
 | 
					S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -135,9 +135,9 @@ class AuthSrv(object):
 | 
				
			|||||||
        self.warn_anonwrite = True
 | 
					        self.warn_anonwrite = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if WINDOWS:
 | 
					        if WINDOWS:
 | 
				
			||||||
            self.re_vol = re.compile(r"^([a-zA-Z]:[\\/][^:]*|[^:]*):([^:]*):(.*)")
 | 
					            self.re_vol = re.compile(r"^([a-zA-Z]:[\\/][^:]*|[^:]*):([^:]*):(.*)$")
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            self.re_vol = re.compile(r"^([^:]*):([^:]*):(.*)")
 | 
					            self.re_vol = re.compile(r"^([^:]*):([^:]*):(.*)$")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.mutex = threading.Lock()
 | 
					        self.mutex = threading.Lock()
 | 
				
			||||||
        self.reload()
 | 
					        self.reload()
 | 
				
			||||||
@@ -220,12 +220,13 @@ class AuthSrv(object):
 | 
				
			|||||||
        if self.args.v:
 | 
					        if self.args.v:
 | 
				
			||||||
            # list of src:dst:permset:permset:...
 | 
					            # list of src:dst:permset:permset:...
 | 
				
			||||||
            # permset is [rwa]username
 | 
					            # permset is [rwa]username
 | 
				
			||||||
            for vol_match in [self.re_vol.match(x) for x in self.args.v]:
 | 
					            for v_str in self.args.v:
 | 
				
			||||||
                try:
 | 
					                m = self.re_vol.match(v_str)
 | 
				
			||||||
                    src, dst, perms = vol_match.groups()
 | 
					                if not m:
 | 
				
			||||||
                except:
 | 
					                    raise Exception("invalid -v argument: [{}]".format(v_str))
 | 
				
			||||||
                    raise Exception("invalid -v argument")
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                src, dst, perms = m.groups()
 | 
				
			||||||
 | 
					                # print("\n".join([src, dst, perms]))
 | 
				
			||||||
                src = fsdec(os.path.abspath(fsenc(src)))
 | 
					                src = fsdec(os.path.abspath(fsenc(src)))
 | 
				
			||||||
                dst = dst.strip("/")
 | 
					                dst = dst.strip("/")
 | 
				
			||||||
                mount[dst] = src
 | 
					                mount[dst] = src
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -29,7 +29,7 @@ class BrokerMp(object):
 | 
				
			|||||||
        self.mutex = threading.Lock()
 | 
					        self.mutex = threading.Lock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        cores = self.args.j
 | 
					        cores = self.args.j
 | 
				
			||||||
        if cores is None:
 | 
					        if not cores:
 | 
				
			||||||
            cores = mp.cpu_count()
 | 
					            cores = mp.cpu_count()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.log("broker", "booting {} subprocesses".format(cores))
 | 
					        self.log("broker", "booting {} subprocesses".format(cores))
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,7 +4,6 @@ from __future__ import print_function, unicode_literals
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import traceback
 | 
					import traceback
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .__init__ import PY2
 | 
					 | 
				
			||||||
from .util import Pebkac, Queue
 | 
					from .util import Pebkac, Queue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,6 +6,8 @@ import stat
 | 
				
			|||||||
import gzip
 | 
					import gzip
 | 
				
			||||||
import time
 | 
					import time
 | 
				
			||||||
import json
 | 
					import json
 | 
				
			||||||
 | 
					import socket
 | 
				
			||||||
 | 
					import ctypes
 | 
				
			||||||
from datetime import datetime
 | 
					from datetime import datetime
 | 
				
			||||||
import calendar
 | 
					import calendar
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -14,9 +16,6 @@ from .util import *  # noqa  # pylint: disable=unused-wildcard-import
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
if not PY2:
 | 
					if not PY2:
 | 
				
			||||||
    unicode = str
 | 
					    unicode = str
 | 
				
			||||||
    from html import escape as html_escape
 | 
					 | 
				
			||||||
else:
 | 
					 | 
				
			||||||
    from cgi import escape as html_escape  # pylint: disable=no-name-in-module
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class HttpCli(object):
 | 
					class HttpCli(object):
 | 
				
			||||||
@@ -25,6 +24,7 @@ class HttpCli(object):
 | 
				
			|||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self, conn):
 | 
					    def __init__(self, conn):
 | 
				
			||||||
 | 
					        self.t0 = time.time()
 | 
				
			||||||
        self.conn = conn
 | 
					        self.conn = conn
 | 
				
			||||||
        self.s = conn.s
 | 
					        self.s = conn.s
 | 
				
			||||||
        self.sr = conn.sr
 | 
					        self.sr = conn.sr
 | 
				
			||||||
@@ -36,13 +36,13 @@ class HttpCli(object):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        self.bufsz = 1024 * 32
 | 
					        self.bufsz = 1024 * 32
 | 
				
			||||||
        self.absolute_urls = False
 | 
					        self.absolute_urls = False
 | 
				
			||||||
        self.out_headers = {}
 | 
					        self.out_headers = {"Access-Control-Allow-Origin": "*"}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def log(self, msg):
 | 
					    def log(self, msg):
 | 
				
			||||||
        self.log_func(self.log_src, msg)
 | 
					        self.log_func(self.log_src, msg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def _check_nonfatal(self, ex):
 | 
					    def _check_nonfatal(self, ex):
 | 
				
			||||||
        return ex.code in [403, 404]
 | 
					        return ex.code in [404]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def _assert_safe_rem(self, rem):
 | 
					    def _assert_safe_rem(self, rem):
 | 
				
			||||||
        # sanity check to prevent any disasters
 | 
					        # sanity check to prevent any disasters
 | 
				
			||||||
@@ -87,7 +87,7 @@ class HttpCli(object):
 | 
				
			|||||||
        if "cookie" in self.headers:
 | 
					        if "cookie" in self.headers:
 | 
				
			||||||
            cookies = self.headers["cookie"].split(";")
 | 
					            cookies = self.headers["cookie"].split(";")
 | 
				
			||||||
            for k, v in [x.split("=", 1) for x in cookies]:
 | 
					            for k, v in [x.split("=", 1) for x in cookies]:
 | 
				
			||||||
                if k != "cppwd":
 | 
					                if k.strip() != "cppwd":
 | 
				
			||||||
                    continue
 | 
					                    continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                v = unescape_cookie(v)
 | 
					                v = unescape_cookie(v)
 | 
				
			||||||
@@ -123,11 +123,20 @@ class HttpCli(object):
 | 
				
			|||||||
        self.uparam = uparam
 | 
					        self.uparam = uparam
 | 
				
			||||||
        self.vpath = unquotep(vpath)
 | 
					        self.vpath = unquotep(vpath)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        ua = self.headers.get("user-agent", "")
 | 
				
			||||||
 | 
					        if ua.startswith("rclone/"):
 | 
				
			||||||
 | 
					            uparam["raw"] = True
 | 
				
			||||||
 | 
					            uparam["dots"] = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            if self.mode in ["GET", "HEAD"]:
 | 
					            if self.mode in ["GET", "HEAD"]:
 | 
				
			||||||
                return self.handle_get() and self.keepalive
 | 
					                return self.handle_get() and self.keepalive
 | 
				
			||||||
            elif self.mode == "POST":
 | 
					            elif self.mode == "POST":
 | 
				
			||||||
                return self.handle_post() and self.keepalive
 | 
					                return self.handle_post() and self.keepalive
 | 
				
			||||||
 | 
					            elif self.mode == "PUT":
 | 
				
			||||||
 | 
					                return self.handle_put() and self.keepalive
 | 
				
			||||||
 | 
					            elif self.mode == "OPTIONS":
 | 
				
			||||||
 | 
					                return self.handle_options() and self.keepalive
 | 
				
			||||||
            else:
 | 
					            else:
 | 
				
			||||||
                raise Pebkac(400, 'invalid HTTP mode "{0}"'.format(self.mode))
 | 
					                raise Pebkac(400, 'invalid HTTP mode "{0}"'.format(self.mode))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -135,7 +144,7 @@ class HttpCli(object):
 | 
				
			|||||||
            try:
 | 
					            try:
 | 
				
			||||||
                # self.log("pebkac at httpcli.run #2: " + repr(ex))
 | 
					                # self.log("pebkac at httpcli.run #2: " + repr(ex))
 | 
				
			||||||
                self.keepalive = self._check_nonfatal(ex)
 | 
					                self.keepalive = self._check_nonfatal(ex)
 | 
				
			||||||
                self.loud_reply(str(ex), status=ex.code)
 | 
					                self.loud_reply("{}: {}".format(str(ex), self.vpath), status=ex.code)
 | 
				
			||||||
                return self.keepalive
 | 
					                return self.keepalive
 | 
				
			||||||
            except Pebkac:
 | 
					            except Pebkac:
 | 
				
			||||||
                return False
 | 
					                return False
 | 
				
			||||||
@@ -143,9 +152,7 @@ class HttpCli(object):
 | 
				
			|||||||
    def send_headers(self, length, status=200, mime=None, headers={}):
 | 
					    def send_headers(self, length, status=200, mime=None, headers={}):
 | 
				
			||||||
        response = ["HTTP/1.1 {} {}".format(status, HTTPCODE[status])]
 | 
					        response = ["HTTP/1.1 {} {}".format(status, HTTPCODE[status])]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if length is None:
 | 
					        if length is not None:
 | 
				
			||||||
            self.keepalive = False
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            response.append("Content-Length: " + str(length))
 | 
					            response.append("Content-Length: " + str(length))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # close if unknown length, otherwise take client's preference
 | 
					        # close if unknown length, otherwise take client's preference
 | 
				
			||||||
@@ -176,7 +183,8 @@ class HttpCli(object):
 | 
				
			|||||||
        self.send_headers(len(body), status, mime, headers)
 | 
					        self.send_headers(len(body), status, mime, headers)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            self.s.sendall(body)
 | 
					            if self.mode != "HEAD":
 | 
				
			||||||
 | 
					                self.s.sendall(body)
 | 
				
			||||||
        except:
 | 
					        except:
 | 
				
			||||||
            raise Pebkac(400, "client d/c while replying body")
 | 
					            raise Pebkac(400, "client d/c while replying body")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -184,7 +192,7 @@ class HttpCli(object):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def loud_reply(self, body, *args, **kwargs):
 | 
					    def loud_reply(self, body, *args, **kwargs):
 | 
				
			||||||
        self.log(body.rstrip())
 | 
					        self.log(body.rstrip())
 | 
				
			||||||
        self.reply(b"<pre>" + body.encode("utf-8"), *list(args), **kwargs)
 | 
					        self.reply(b"<pre>" + body.encode("utf-8") + b"\r\n", *list(args), **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def handle_get(self):
 | 
					    def handle_get(self):
 | 
				
			||||||
        logmsg = "{:4} {}".format(self.mode, self.req)
 | 
					        logmsg = "{:4} {}".format(self.mode, self.req)
 | 
				
			||||||
@@ -230,6 +238,30 @@ class HttpCli(object):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        return self.tx_browser()
 | 
					        return self.tx_browser()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def handle_options(self):
 | 
				
			||||||
 | 
					        self.log("OPTIONS " + self.req)
 | 
				
			||||||
 | 
					        self.send_headers(
 | 
				
			||||||
 | 
					            None,
 | 
				
			||||||
 | 
					            204,
 | 
				
			||||||
 | 
					            headers={
 | 
				
			||||||
 | 
					                "Access-Control-Allow-Origin": "*",
 | 
				
			||||||
 | 
					                "Access-Control-Allow-Methods": "*",
 | 
				
			||||||
 | 
					                "Access-Control-Allow-Headers": "*",
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def handle_put(self):
 | 
				
			||||||
 | 
					        self.log("PUT " + self.req)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if self.headers.get("expect", "").lower() == "100-continue":
 | 
				
			||||||
 | 
					            try:
 | 
				
			||||||
 | 
					                self.s.sendall(b"HTTP/1.1 100 Continue\r\n\r\n")
 | 
				
			||||||
 | 
					            except:
 | 
				
			||||||
 | 
					                raise Pebkac(400, "client d/c before 100 continue")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return self.handle_stash()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def handle_post(self):
 | 
					    def handle_post(self):
 | 
				
			||||||
        self.log("POST " + self.req)
 | 
					        self.log("POST " + self.req)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -243,6 +275,9 @@ class HttpCli(object):
 | 
				
			|||||||
        if not ctype:
 | 
					        if not ctype:
 | 
				
			||||||
            raise Pebkac(400, "you can't post without a content-type header")
 | 
					            raise Pebkac(400, "you can't post without a content-type header")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if "raw" in self.uparam:
 | 
				
			||||||
 | 
					            return self.handle_stash()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if "multipart/form-data" in ctype:
 | 
					        if "multipart/form-data" in ctype:
 | 
				
			||||||
            return self.handle_post_multipart()
 | 
					            return self.handle_post_multipart()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -255,6 +290,37 @@ class HttpCli(object):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        raise Pebkac(405, "don't know how to handle {} POST".format(ctype))
 | 
					        raise Pebkac(405, "don't know how to handle {} POST".format(ctype))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def handle_stash(self):
 | 
				
			||||||
 | 
					        remains = int(self.headers.get("content-length", None))
 | 
				
			||||||
 | 
					        if remains is None:
 | 
				
			||||||
 | 
					            reader = read_socket_unbounded(self.sr)
 | 
				
			||||||
 | 
					            self.keepalive = False
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            reader = read_socket(self.sr, remains)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        vfs, rem = self.conn.auth.vfs.get(self.vpath, self.uname, False, True)
 | 
				
			||||||
 | 
					        fdir = os.path.join(vfs.realpath, rem)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        addr = self.conn.addr[0].replace(":", ".")
 | 
				
			||||||
 | 
					        fn = "put-{:.6f}-{}.bin".format(time.time(), addr)
 | 
				
			||||||
 | 
					        path = os.path.join(fdir, fn)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        with open(path, "wb", 512 * 1024) as f:
 | 
				
			||||||
 | 
					            post_sz, _, sha_b64 = hashcopy(self.conn, reader, f)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        spd = self._spd(post_sz)
 | 
				
			||||||
 | 
					        self.log("{} wrote {}/{} bytes to {}".format(spd, post_sz, remains, path))
 | 
				
			||||||
 | 
					        self.reply("{}\n{}\n".format(post_sz, sha_b64).encode("utf-8"))
 | 
				
			||||||
 | 
					        return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _spd(self, nbytes, add=True):
 | 
				
			||||||
 | 
					        if add:
 | 
				
			||||||
 | 
					            self.conn.nbyte += nbytes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        spd1 = get_spd(nbytes, self.t0)
 | 
				
			||||||
 | 
					        spd2 = get_spd(self.conn.nbyte, self.conn.t0)
 | 
				
			||||||
 | 
					        return spd1 + " " + spd2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def handle_post_multipart(self):
 | 
					    def handle_post_multipart(self):
 | 
				
			||||||
        self.parser = MultipartParser(self.log, self.sr, self.headers)
 | 
					        self.parser = MultipartParser(self.log, self.sr, self.headers)
 | 
				
			||||||
        self.parser.parse()
 | 
					        self.parser.parse()
 | 
				
			||||||
@@ -394,7 +460,9 @@ class HttpCli(object):
 | 
				
			|||||||
            except:
 | 
					            except:
 | 
				
			||||||
                self.log("failed to utime ({}, {})".format(path, times))
 | 
					                self.log("failed to utime ({}, {})".format(path, times))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.loud_reply("thank")
 | 
					        spd = self._spd(post_sz)
 | 
				
			||||||
 | 
					        self.log("{} thank".format(spd))
 | 
				
			||||||
 | 
					        self.reply("thank")
 | 
				
			||||||
        return True
 | 
					        return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def handle_login(self):
 | 
					    def handle_login(self):
 | 
				
			||||||
@@ -407,7 +475,7 @@ class HttpCli(object):
 | 
				
			|||||||
            msg = "naw dude"
 | 
					            msg = "naw dude"
 | 
				
			||||||
            pwd = "x"  # nosec
 | 
					            pwd = "x"  # nosec
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        h = {"Set-Cookie": "cppwd={}; Path=/".format(pwd)}
 | 
					        h = {"Set-Cookie": "cppwd={}; Path=/; SameSite=Lax".format(pwd)}
 | 
				
			||||||
        html = self.conn.tpl_msg.render(h1=msg, h2='<a href="/">ack</a>', redir="/")
 | 
					        html = self.conn.tpl_msg.render(h1=msg, h2='<a href="/">ack</a>', redir="/")
 | 
				
			||||||
        self.reply(html.encode("utf-8"), headers=h)
 | 
					        self.reply(html.encode("utf-8"), headers=h)
 | 
				
			||||||
        return True
 | 
					        return True
 | 
				
			||||||
@@ -420,9 +488,11 @@ class HttpCli(object):
 | 
				
			|||||||
        vfs, rem = self.conn.auth.vfs.get(self.vpath, self.uname, False, True)
 | 
					        vfs, rem = self.conn.auth.vfs.get(self.vpath, self.uname, False, True)
 | 
				
			||||||
        self._assert_safe_rem(rem)
 | 
					        self._assert_safe_rem(rem)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        sanitized = sanitize_fn(new_dir)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if not nullwrite:
 | 
					        if not nullwrite:
 | 
				
			||||||
            fdir = os.path.join(vfs.realpath, rem)
 | 
					            fdir = os.path.join(vfs.realpath, rem)
 | 
				
			||||||
            fn = os.path.join(fdir, sanitize_fn(new_dir))
 | 
					            fn = os.path.join(fdir, sanitized)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if not os.path.isdir(fsenc(fdir)):
 | 
					            if not os.path.isdir(fsenc(fdir)):
 | 
				
			||||||
                raise Pebkac(500, "parent folder does not exist")
 | 
					                raise Pebkac(500, "parent folder does not exist")
 | 
				
			||||||
@@ -435,10 +505,10 @@ class HttpCli(object):
 | 
				
			|||||||
            except:
 | 
					            except:
 | 
				
			||||||
                raise Pebkac(500, "mkdir failed, check the logs")
 | 
					                raise Pebkac(500, "mkdir failed, check the logs")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        vpath = "{}/{}".format(self.vpath, new_dir).lstrip("/")
 | 
					        vpath = "{}/{}".format(self.vpath, sanitized).lstrip("/")
 | 
				
			||||||
        html = self.conn.tpl_msg.render(
 | 
					        html = self.conn.tpl_msg.render(
 | 
				
			||||||
            h2='<a href="/{}">go to /{}</a>'.format(
 | 
					            h2='<a href="/{}">go to /{}</a>'.format(
 | 
				
			||||||
                quotep(vpath), html_escape(vpath, quote=False)
 | 
					                quotep(vpath), html_escape(vpath)
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
            pre="aight",
 | 
					            pre="aight",
 | 
				
			||||||
            click=True,
 | 
					            click=True,
 | 
				
			||||||
@@ -457,9 +527,11 @@ class HttpCli(object):
 | 
				
			|||||||
        if not new_file.endswith(".md"):
 | 
					        if not new_file.endswith(".md"):
 | 
				
			||||||
            new_file += ".md"
 | 
					            new_file += ".md"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        sanitized = sanitize_fn(new_file)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if not nullwrite:
 | 
					        if not nullwrite:
 | 
				
			||||||
            fdir = os.path.join(vfs.realpath, rem)
 | 
					            fdir = os.path.join(vfs.realpath, rem)
 | 
				
			||||||
            fn = os.path.join(fdir, sanitize_fn(new_file))
 | 
					            fn = os.path.join(fdir, sanitized)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if os.path.exists(fsenc(fn)):
 | 
					            if os.path.exists(fsenc(fn)):
 | 
				
			||||||
                raise Pebkac(500, "that file exists already")
 | 
					                raise Pebkac(500, "that file exists already")
 | 
				
			||||||
@@ -467,10 +539,10 @@ class HttpCli(object):
 | 
				
			|||||||
            with open(fsenc(fn), "wb") as f:
 | 
					            with open(fsenc(fn), "wb") as f:
 | 
				
			||||||
                f.write(b"`GRUNNUR`\n")
 | 
					                f.write(b"`GRUNNUR`\n")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        vpath = "{}/{}".format(self.vpath, new_file).lstrip("/")
 | 
					        vpath = "{}/{}".format(self.vpath, sanitized).lstrip("/")
 | 
				
			||||||
        html = self.conn.tpl_msg.render(
 | 
					        html = self.conn.tpl_msg.render(
 | 
				
			||||||
            h2='<a href="/{}?edit">go to /{}?edit</a>'.format(
 | 
					            h2='<a href="/{}?edit">go to /{}?edit</a>'.format(
 | 
				
			||||||
                quotep(vpath), html_escape(vpath, quote=False)
 | 
					                quotep(vpath), html_escape(vpath)
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
            pre="aight",
 | 
					            pre="aight",
 | 
				
			||||||
            click=True,
 | 
					            click=True,
 | 
				
			||||||
@@ -515,6 +587,7 @@ class HttpCli(object):
 | 
				
			|||||||
                            raise Pebkac(400, "empty files in post")
 | 
					                            raise Pebkac(400, "empty files in post")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                        files.append([sz, sha512_hex])
 | 
					                        files.append([sz, sha512_hex])
 | 
				
			||||||
 | 
					                        self.conn.nbyte += sz
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                except Pebkac:
 | 
					                except Pebkac:
 | 
				
			||||||
                    if fn != os.devnull:
 | 
					                    if fn != os.devnull:
 | 
				
			||||||
@@ -542,7 +615,9 @@ class HttpCli(object):
 | 
				
			|||||||
            # truncated SHA-512 prevents length extension attacks;
 | 
					            # truncated SHA-512 prevents length extension attacks;
 | 
				
			||||||
            # using SHA-512/224, optionally SHA-512/256 = :64
 | 
					            # using SHA-512/224, optionally SHA-512/256 = :64
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.log(msg)
 | 
					        vspd = self._spd(sz_total, False)
 | 
				
			||||||
 | 
					        self.log("{} {}".format(vspd, msg))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if not nullwrite:
 | 
					        if not nullwrite:
 | 
				
			||||||
            # TODO this is bad
 | 
					            # TODO this is bad
 | 
				
			||||||
            log_fn = "up.{:.6f}.txt".format(t0)
 | 
					            log_fn = "up.{:.6f}.txt".format(t0)
 | 
				
			||||||
@@ -564,7 +639,7 @@ class HttpCli(object):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        html = self.conn.tpl_msg.render(
 | 
					        html = self.conn.tpl_msg.render(
 | 
				
			||||||
            h2='<a href="/{}">return to /{}</a>'.format(
 | 
					            h2='<a href="/{}">return to /{}</a>'.format(
 | 
				
			||||||
                quotep(self.vpath), html_escape(self.vpath, quote=False)
 | 
					                quotep(self.vpath), html_escape(self.vpath)
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
            pre=msg,
 | 
					            pre=msg,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
@@ -600,10 +675,10 @@ class HttpCli(object):
 | 
				
			|||||||
            self.reply(response.encode("utf-8"))
 | 
					            self.reply(response.encode("utf-8"))
 | 
				
			||||||
            return True
 | 
					            return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        fn = os.path.join(vfs.realpath, rem)
 | 
					        fp = os.path.join(vfs.realpath, rem)
 | 
				
			||||||
        srv_lastmod = -1
 | 
					        srv_lastmod = -1
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            st = os.stat(fsenc(fn))
 | 
					            st = os.stat(fsenc(fp))
 | 
				
			||||||
            srv_lastmod = st.st_mtime
 | 
					            srv_lastmod = st.st_mtime
 | 
				
			||||||
            srv_lastmod3 = int(srv_lastmod * 1000)
 | 
					            srv_lastmod3 = int(srv_lastmod * 1000)
 | 
				
			||||||
        except OSError as ex:
 | 
					        except OSError as ex:
 | 
				
			||||||
@@ -612,7 +687,16 @@ class HttpCli(object):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        # if file exists, chekc that timestamp matches the client's
 | 
					        # if file exists, chekc that timestamp matches the client's
 | 
				
			||||||
        if srv_lastmod >= 0:
 | 
					        if srv_lastmod >= 0:
 | 
				
			||||||
            if cli_lastmod3 not in [-1, srv_lastmod3]:
 | 
					            same_lastmod = cli_lastmod3 in [-1, srv_lastmod3]
 | 
				
			||||||
 | 
					            if not same_lastmod:
 | 
				
			||||||
 | 
					                # some filesystems/transports limit precision to 1sec, hopefully floored
 | 
				
			||||||
 | 
					                same_lastmod = (
 | 
				
			||||||
 | 
					                    srv_lastmod == int(srv_lastmod)
 | 
				
			||||||
 | 
					                    and cli_lastmod3 > srv_lastmod3
 | 
				
			||||||
 | 
					                    and cli_lastmod3 - srv_lastmod3 < 1000
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if not same_lastmod:
 | 
				
			||||||
                response = json.dumps(
 | 
					                response = json.dumps(
 | 
				
			||||||
                    {
 | 
					                    {
 | 
				
			||||||
                        "ok": False,
 | 
					                        "ok": False,
 | 
				
			||||||
@@ -631,16 +715,22 @@ class HttpCli(object):
 | 
				
			|||||||
                return True
 | 
					                return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # TODO another hack re: pending permissions rework
 | 
					            # TODO another hack re: pending permissions rework
 | 
				
			||||||
            os.rename(fn, "{}.bak-{:.3f}.md".format(fn[:-3], srv_lastmod))
 | 
					            mdir, mfile = os.path.split(fp)
 | 
				
			||||||
 | 
					            mfile2 = "{}.{:.3f}.md".format(mfile[:-3], srv_lastmod)
 | 
				
			||||||
 | 
					            try:
 | 
				
			||||||
 | 
					                os.mkdir(os.path.join(mdir, ".hist"))
 | 
				
			||||||
 | 
					            except:
 | 
				
			||||||
 | 
					                pass
 | 
				
			||||||
 | 
					            os.rename(fp, os.path.join(mdir, ".hist", mfile2))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        p_field, _, p_data = next(self.parser.gen)
 | 
					        p_field, _, p_data = next(self.parser.gen)
 | 
				
			||||||
        if p_field != "body":
 | 
					        if p_field != "body":
 | 
				
			||||||
            raise Pebkac(400, "expected body, got {}".format(p_field))
 | 
					            raise Pebkac(400, "expected body, got {}".format(p_field))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        with open(fn, "wb") as f:
 | 
					        with open(fp, "wb") as f:
 | 
				
			||||||
            sz, sha512, _ = hashcopy(self.conn, p_data, f)
 | 
					            sz, sha512, _ = hashcopy(self.conn, p_data, f)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        new_lastmod = os.stat(fsenc(fn)).st_mtime
 | 
					        new_lastmod = os.stat(fsenc(fp)).st_mtime
 | 
				
			||||||
        new_lastmod3 = int(new_lastmod * 1000)
 | 
					        new_lastmod3 = int(new_lastmod * 1000)
 | 
				
			||||||
        sha512 = sha512[:56]
 | 
					        sha512 = sha512[:56]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -759,11 +849,20 @@ class HttpCli(object):
 | 
				
			|||||||
                else:
 | 
					                else:
 | 
				
			||||||
                    upper = file_sz
 | 
					                    upper = file_sz
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if lower < 0 or lower >= file_sz or upper < 0 or upper > file_sz:
 | 
					                if upper > file_sz:
 | 
				
			||||||
 | 
					                    upper = file_sz
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if lower < 0 or lower >= upper:
 | 
				
			||||||
                    raise Exception()
 | 
					                    raise Exception()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            except:
 | 
					            except:
 | 
				
			||||||
                raise Pebkac(400, "invalid range requested: " + hrange)
 | 
					                err = "invalid range ({}), size={}".format(hrange, file_sz)
 | 
				
			||||||
 | 
					                self.loud_reply(
 | 
				
			||||||
 | 
					                    err,
 | 
				
			||||||
 | 
					                    status=416,
 | 
				
			||||||
 | 
					                    headers={"Content-Range": "bytes */{}".format(file_sz)},
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            status = 206
 | 
					            status = 206
 | 
				
			||||||
            self.out_headers["Content-Range"] = "bytes {}-{}/{}".format(
 | 
					            self.out_headers["Content-Range"] = "bytes {}-{}/{}".format(
 | 
				
			||||||
@@ -785,6 +884,9 @@ class HttpCli(object):
 | 
				
			|||||||
        #
 | 
					        #
 | 
				
			||||||
        # send reply
 | 
					        # send reply
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if not is_compressed:
 | 
				
			||||||
 | 
					            self.out_headers["Cache-Control"] = "no-cache"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.out_headers["Accept-Ranges"] = "bytes"
 | 
					        self.out_headers["Accept-Ranges"] = "bytes"
 | 
				
			||||||
        self.send_headers(
 | 
					        self.send_headers(
 | 
				
			||||||
            length=upper - lower,
 | 
					            length=upper - lower,
 | 
				
			||||||
@@ -798,6 +900,7 @@ class HttpCli(object):
 | 
				
			|||||||
            self.log(logmsg)
 | 
					            self.log(logmsg)
 | 
				
			||||||
            return True
 | 
					            return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        ret = True
 | 
				
			||||||
        with open_func(*open_args) as f:
 | 
					        with open_func(*open_args) as f:
 | 
				
			||||||
            remains = upper - lower
 | 
					            remains = upper - lower
 | 
				
			||||||
            f.seek(lower)
 | 
					            f.seek(lower)
 | 
				
			||||||
@@ -810,21 +913,21 @@ class HttpCli(object):
 | 
				
			|||||||
                if remains < len(buf):
 | 
					                if remains < len(buf):
 | 
				
			||||||
                    buf = buf[:remains]
 | 
					                    buf = buf[:remains]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                remains -= len(buf)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                try:
 | 
					                try:
 | 
				
			||||||
                    self.s.sendall(buf)
 | 
					                    self.s.sendall(buf)
 | 
				
			||||||
 | 
					                    remains -= len(buf)
 | 
				
			||||||
                except:
 | 
					                except:
 | 
				
			||||||
                    logmsg += " \033[31m" + str(upper - remains) + "\033[0m"
 | 
					                    logmsg += " \033[31m" + str(upper - remains) + "\033[0m"
 | 
				
			||||||
                    self.log(logmsg)
 | 
					                    ret = False
 | 
				
			||||||
                    return False
 | 
					                    break
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.log(logmsg)
 | 
					        spd = self._spd((upper - lower) - remains)
 | 
				
			||||||
        return True
 | 
					        self.log("{},  {}".format(logmsg, spd))
 | 
				
			||||||
 | 
					        return ret
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def tx_md(self, fs_path):
 | 
					    def tx_md(self, fs_path):
 | 
				
			||||||
        logmsg = "{:4} {} ".format("", self.req)
 | 
					        logmsg = "{:4} {} ".format("", self.req)
 | 
				
			||||||
        if "edit" in self.uparam:
 | 
					        if "edit2" in self.uparam:
 | 
				
			||||||
            html_path = "web/mde.html"
 | 
					            html_path = "web/mde.html"
 | 
				
			||||||
            template = self.conn.tpl_mde
 | 
					            template = self.conn.tpl_mde
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
@@ -834,19 +937,27 @@ class HttpCli(object):
 | 
				
			|||||||
        html_path = os.path.join(E.mod, html_path)
 | 
					        html_path = os.path.join(E.mod, html_path)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        st = os.stat(fsenc(fs_path))
 | 
					        st = os.stat(fsenc(fs_path))
 | 
				
			||||||
        sz_md = st.st_size
 | 
					        # sz_md = st.st_size
 | 
				
			||||||
        ts_md = st.st_mtime
 | 
					        ts_md = st.st_mtime
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        st = os.stat(fsenc(html_path))
 | 
					        st = os.stat(fsenc(html_path))
 | 
				
			||||||
        ts_html = st.st_mtime
 | 
					        ts_html = st.st_mtime
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # TODO dont load into memory ;_;
 | 
				
			||||||
 | 
					        #   (trivial fix, count the &'s)
 | 
				
			||||||
 | 
					        with open(fsenc(fs_path), "rb") as f:
 | 
				
			||||||
 | 
					            md = f.read().replace(b"&", b"&")
 | 
				
			||||||
 | 
					            sz_md = len(md)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        file_ts = max(ts_md, ts_html)
 | 
					        file_ts = max(ts_md, ts_html)
 | 
				
			||||||
        file_lastmod, do_send = self._chk_lastmod(file_ts)
 | 
					        file_lastmod, do_send = self._chk_lastmod(file_ts)
 | 
				
			||||||
        self.out_headers["Last-Modified"] = file_lastmod
 | 
					        self.out_headers["Last-Modified"] = file_lastmod
 | 
				
			||||||
 | 
					        self.out_headers["Cache-Control"] = "no-cache"
 | 
				
			||||||
        status = 200 if do_send else 304
 | 
					        status = 200 if do_send else 304
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        targs = {
 | 
					        targs = {
 | 
				
			||||||
            "title": html_escape(self.vpath, quote=False),
 | 
					            "edit": "edit" in self.uparam,
 | 
				
			||||||
 | 
					            "title": html_escape(self.vpath),
 | 
				
			||||||
            "lastmod": int(ts_md * 1000),
 | 
					            "lastmod": int(ts_md * 1000),
 | 
				
			||||||
            "md": "",
 | 
					            "md": "",
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -858,9 +969,7 @@ class HttpCli(object):
 | 
				
			|||||||
            self.log(logmsg)
 | 
					            self.log(logmsg)
 | 
				
			||||||
            return True
 | 
					            return True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        with open(fsenc(fs_path), "rb") as f:
 | 
					        # TODO jinja2 can stream this right?
 | 
				
			||||||
            md = f.read()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        targs["md"] = md.decode("utf-8", "replace")
 | 
					        targs["md"] = md.decode("utf-8", "replace")
 | 
				
			||||||
        html = template.render(**targs).encode("utf-8")
 | 
					        html = template.render(**targs).encode("utf-8")
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
@@ -889,7 +998,7 @@ class HttpCli(object):
 | 
				
			|||||||
                else:
 | 
					                else:
 | 
				
			||||||
                    vpath += "/" + node
 | 
					                    vpath += "/" + node
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                vpnodes.append([quotep(vpath) + "/", html_escape(node, quote=False)])
 | 
					                vpnodes.append([quotep(vpath) + "/", html_escape(node)])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        vn, rem = self.auth.vfs.get(
 | 
					        vn, rem = self.auth.vfs.get(
 | 
				
			||||||
            self.vpath, self.uname, self.readable, self.writable
 | 
					            self.vpath, self.uname, self.readable, self.writable
 | 
				
			||||||
@@ -909,12 +1018,34 @@ class HttpCli(object):
 | 
				
			|||||||
        fsroot, vfs_ls, vfs_virt = vn.ls(rem, self.uname)
 | 
					        fsroot, vfs_ls, vfs_virt = vn.ls(rem, self.uname)
 | 
				
			||||||
        vfs_ls.extend(vfs_virt.keys())
 | 
					        vfs_ls.extend(vfs_virt.keys())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # check for old versions of files,
 | 
				
			||||||
 | 
					        hist = {}  # [num-backups, most-recent, hist-path]
 | 
				
			||||||
 | 
					        histdir = os.path.join(fsroot, ".hist")
 | 
				
			||||||
 | 
					        ptn = re.compile(r"(.*)\.([0-9]+\.[0-9]{3})(\.[^\.]+)$")
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            for hfn in os.listdir(histdir):
 | 
				
			||||||
 | 
					                m = ptn.match(hfn)
 | 
				
			||||||
 | 
					                if not m:
 | 
				
			||||||
 | 
					                    continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                fn = m.group(1) + m.group(3)
 | 
				
			||||||
 | 
					                n, ts, _ = hist.get(fn, [0, 0, ""])
 | 
				
			||||||
 | 
					                hist[fn] = [n + 1, max(ts, float(m.group(2))), hfn]
 | 
				
			||||||
 | 
					        except:
 | 
				
			||||||
 | 
					            pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # show dotfiles if permitted and requested
 | 
				
			||||||
 | 
					        if not self.args.ed or "dots" not in self.uparam:
 | 
				
			||||||
 | 
					            vfs_ls = exclude_dotfiles(vfs_ls)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        dirs = []
 | 
					        dirs = []
 | 
				
			||||||
        files = []
 | 
					        files = []
 | 
				
			||||||
        for fn in exclude_dotfiles(vfs_ls):
 | 
					        for fn in vfs_ls:
 | 
				
			||||||
 | 
					            base = ""
 | 
				
			||||||
            href = fn
 | 
					            href = fn
 | 
				
			||||||
            if self.absolute_urls and vpath:
 | 
					            if self.absolute_urls and vpath:
 | 
				
			||||||
                href = "/" + vpath + "/" + fn
 | 
					                base = "/" + vpath + "/"
 | 
				
			||||||
 | 
					                href = base + fn
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if fn in vfs_virt:
 | 
					            if fn in vfs_virt:
 | 
				
			||||||
                fspath = vfs_virt[fn].realpath
 | 
					                fspath = vfs_virt[fn].realpath
 | 
				
			||||||
@@ -931,6 +1062,10 @@ class HttpCli(object):
 | 
				
			|||||||
            if is_dir:
 | 
					            if is_dir:
 | 
				
			||||||
                margin = "DIR"
 | 
					                margin = "DIR"
 | 
				
			||||||
                href += "/"
 | 
					                href += "/"
 | 
				
			||||||
 | 
					            elif fn in hist:
 | 
				
			||||||
 | 
					                margin = '<a href="{}.hist/{}">#{}</a>'.format(
 | 
				
			||||||
 | 
					                    base, html_escape(hist[fn][2], quote=True), hist[fn][0]
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
            else:
 | 
					            else:
 | 
				
			||||||
                margin = "-"
 | 
					                margin = "-"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -938,7 +1073,7 @@ class HttpCli(object):
 | 
				
			|||||||
            dt = datetime.utcfromtimestamp(inf.st_mtime)
 | 
					            dt = datetime.utcfromtimestamp(inf.st_mtime)
 | 
				
			||||||
            dt = dt.strftime("%Y-%m-%d %H:%M:%S")
 | 
					            dt = dt.strftime("%Y-%m-%d %H:%M:%S")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            item = [margin, quotep(href), html_escape(fn, quote=False), sz, dt]
 | 
					            item = [margin, quotep(href), html_escape(fn), sz, dt]
 | 
				
			||||||
            if is_dir:
 | 
					            if is_dir:
 | 
				
			||||||
                dirs.append(item)
 | 
					                dirs.append(item)
 | 
				
			||||||
            else:
 | 
					            else:
 | 
				
			||||||
@@ -951,6 +1086,45 @@ class HttpCli(object):
 | 
				
			|||||||
                with open(fsenc(fn), "rb") as f:
 | 
					                with open(fsenc(fn), "rb") as f:
 | 
				
			||||||
                    logues[n] = f.read().decode("utf-8")
 | 
					                    logues[n] = f.read().decode("utf-8")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if False:
 | 
				
			||||||
 | 
					            # this is a mistake
 | 
				
			||||||
 | 
					            md = None
 | 
				
			||||||
 | 
					            for fn in [x[2] for x in files]:
 | 
				
			||||||
 | 
					                if fn.lower() == "readme.md":
 | 
				
			||||||
 | 
					                    fn = os.path.join(abspath, fn)
 | 
				
			||||||
 | 
					                    with open(fn, "rb") as f:
 | 
				
			||||||
 | 
					                        md = f.read().decode("utf-8")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    break
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        srv_info = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            if not self.args.nih:
 | 
				
			||||||
 | 
					                srv_info.append(str(socket.gethostname()).split(".")[0])
 | 
				
			||||||
 | 
					        except:
 | 
				
			||||||
 | 
					            self.log("#wow #whoa")
 | 
				
			||||||
 | 
					            pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            # some fuses misbehave
 | 
				
			||||||
 | 
					            if not self.args.nid:
 | 
				
			||||||
 | 
					                if WINDOWS:
 | 
				
			||||||
 | 
					                    bfree = ctypes.c_ulonglong(0)
 | 
				
			||||||
 | 
					                    ctypes.windll.kernel32.GetDiskFreeSpaceExW(
 | 
				
			||||||
 | 
					                        ctypes.c_wchar_p(abspath), None, None, ctypes.pointer(bfree)
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                    srv_info.append(humansize(bfree.value) + " free")
 | 
				
			||||||
 | 
					                else:
 | 
				
			||||||
 | 
					                    sv = os.statvfs(abspath)
 | 
				
			||||||
 | 
					                    free = humansize(sv.f_frsize * sv.f_bfree, True)
 | 
				
			||||||
 | 
					                    total = humansize(sv.f_frsize * sv.f_blocks, True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    srv_info.append(free + " free")
 | 
				
			||||||
 | 
					                    srv_info.append(total)
 | 
				
			||||||
 | 
					        except:
 | 
				
			||||||
 | 
					            pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        ts = ""
 | 
					        ts = ""
 | 
				
			||||||
        # ts = "?{}".format(time.time())
 | 
					        # ts = "?{}".format(time.time())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -964,7 +1138,8 @@ class HttpCli(object):
 | 
				
			|||||||
            ts=ts,
 | 
					            ts=ts,
 | 
				
			||||||
            prologue=logues[0],
 | 
					            prologue=logues[0],
 | 
				
			||||||
            epilogue=logues[1],
 | 
					            epilogue=logues[1],
 | 
				
			||||||
            title=html_escape(self.vpath, quote=False),
 | 
					            title=html_escape(self.vpath),
 | 
				
			||||||
 | 
					            srv_info="</span> /// <span>".join(srv_info),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        self.reply(html.encode("utf-8", "replace"))
 | 
					        self.reply(html.encode("utf-8", "replace"))
 | 
				
			||||||
        return True
 | 
					        return True
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,6 +4,7 @@ from __future__ import print_function, unicode_literals
 | 
				
			|||||||
import os
 | 
					import os
 | 
				
			||||||
import sys
 | 
					import sys
 | 
				
			||||||
import ssl
 | 
					import ssl
 | 
				
			||||||
 | 
					import time
 | 
				
			||||||
import socket
 | 
					import socket
 | 
				
			||||||
 | 
					
 | 
				
			||||||
try:
 | 
					try:
 | 
				
			||||||
@@ -41,6 +42,8 @@ class HttpConn(object):
 | 
				
			|||||||
        self.auth = hsrv.auth
 | 
					        self.auth = hsrv.auth
 | 
				
			||||||
        self.cert_path = hsrv.cert_path
 | 
					        self.cert_path = hsrv.cert_path
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.t0 = time.time()
 | 
				
			||||||
 | 
					        self.nbyte = 0
 | 
				
			||||||
        self.workload = 0
 | 
					        self.workload = 0
 | 
				
			||||||
        self.log_func = hsrv.log
 | 
					        self.log_func = hsrv.log
 | 
				
			||||||
        self.log_src = "{} \033[36m{}".format(addr[0], addr[1]).ljust(26)
 | 
					        self.log_src = "{} \033[36m{}".format(addr[0], addr[1]).ljust(26)
 | 
				
			||||||
@@ -86,7 +89,7 @@ class HttpConn(object):
 | 
				
			|||||||
                self.s.send(b"HTTP/1.1 400 Bad Request\r\n\r\n" + err.encode("utf-8"))
 | 
					                self.s.send(b"HTTP/1.1 400 Bad Request\r\n\r\n" + err.encode("utf-8"))
 | 
				
			||||||
                return
 | 
					                return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if method not in [None, b"GET ", b"HEAD", b"POST"]:
 | 
					        if method not in [None, b"GET ", b"HEAD", b"POST", b"PUT ", b"OPTI"]:
 | 
				
			||||||
            if self.sr:
 | 
					            if self.sr:
 | 
				
			||||||
                self.log("\033[1;31mTODO: cannot do https in jython\033[0m")
 | 
					                self.log("\033[1;31mTODO: cannot do https in jython\033[0m")
 | 
				
			||||||
                return
 | 
					                return
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,7 +6,7 @@ import time
 | 
				
			|||||||
import socket
 | 
					import socket
 | 
				
			||||||
import threading
 | 
					import threading
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .__init__ import E
 | 
					from .__init__ import E, MACOS
 | 
				
			||||||
from .httpconn import HttpConn
 | 
					from .httpconn import HttpConn
 | 
				
			||||||
from .authsrv import AuthSrv
 | 
					from .authsrv import AuthSrv
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -75,11 +75,14 @@ class HttpSrv(object):
 | 
				
			|||||||
                sck.shutdown(socket.SHUT_RDWR)
 | 
					                sck.shutdown(socket.SHUT_RDWR)
 | 
				
			||||||
                sck.close()
 | 
					                sck.close()
 | 
				
			||||||
            except (OSError, socket.error) as ex:
 | 
					            except (OSError, socket.error) as ex:
 | 
				
			||||||
                self.log(
 | 
					                if not MACOS:
 | 
				
			||||||
                    "%s %s" % addr, "shut_rdwr err:\n  {}\n  {}".format(repr(sck), ex),
 | 
					                    self.log(
 | 
				
			||||||
                )
 | 
					                        "%s %s" % addr,
 | 
				
			||||||
                if ex.errno not in [10038, 107, 57, 9]:
 | 
					                        "shut_rdwr err:\n  {}\n  {}".format(repr(sck), ex),
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                if ex.errno not in [10038, 10054, 107, 57, 9]:
 | 
				
			||||||
                    # 10038 No longer considered a socket
 | 
					                    # 10038 No longer considered a socket
 | 
				
			||||||
 | 
					                    # 10054 Foribly closed by remote
 | 
				
			||||||
                    #   107 Transport endpoint not connected
 | 
					                    #   107 Transport endpoint not connected
 | 
				
			||||||
                    #    57 Socket is not connected
 | 
					                    #    57 Socket is not connected
 | 
				
			||||||
                    #     9 Bad file descriptor
 | 
					                    #     9 Bad file descriptor
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,7 +8,7 @@ import threading
 | 
				
			|||||||
from datetime import datetime, timedelta
 | 
					from datetime import datetime, timedelta
 | 
				
			||||||
import calendar
 | 
					import calendar
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .__init__ import PY2, WINDOWS, VT100
 | 
					from .__init__ import PY2, WINDOWS, MACOS, VT100
 | 
				
			||||||
from .tcpsrv import TcpSrv
 | 
					from .tcpsrv import TcpSrv
 | 
				
			||||||
from .up2k import Up2k
 | 
					from .up2k import Up2k
 | 
				
			||||||
from .util import mp
 | 
					from .util import mp
 | 
				
			||||||
@@ -111,6 +111,8 @@ class SvcHub(object):
 | 
				
			|||||||
                return msg
 | 
					                return msg
 | 
				
			||||||
            elif vmin < 3:
 | 
					            elif vmin < 3:
 | 
				
			||||||
                return msg
 | 
					                return msg
 | 
				
			||||||
 | 
					        elif MACOS:
 | 
				
			||||||
 | 
					            return "multiprocessing is wonky on mac osx;"
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            msg = "need python 2.7 or 3.3+ for multiprocessing;"
 | 
					            msg = "need python 2.7 or 3.3+ for multiprocessing;"
 | 
				
			||||||
            if not PY2 and vmin < 3:
 | 
					            if not PY2 and vmin < 3:
 | 
				
			||||||
@@ -127,8 +129,8 @@ class SvcHub(object):
 | 
				
			|||||||
        return None
 | 
					        return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def check_mp_enable(self):
 | 
					    def check_mp_enable(self):
 | 
				
			||||||
        if self.args.j == 0:
 | 
					        if self.args.j == 1:
 | 
				
			||||||
            self.log("root", "multiprocessing disabled by argument -j 0;")
 | 
					            self.log("root", "multiprocessing disabled by argument -j 1;")
 | 
				
			||||||
            return False
 | 
					            return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if mp.cpu_count() <= 1:
 | 
					        if mp.cpu_count() <= 1:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -24,7 +24,7 @@ class TcpSrv(object):
 | 
				
			|||||||
        ip = "127.0.0.1"
 | 
					        ip = "127.0.0.1"
 | 
				
			||||||
        eps = {ip: "local only"}
 | 
					        eps = {ip: "local only"}
 | 
				
			||||||
        if self.args.i != ip:
 | 
					        if self.args.i != ip:
 | 
				
			||||||
            eps = self.detect_interfaces(self.args.i) or eps
 | 
					            eps = self.detect_interfaces(self.args.i) or {self.args.i: "external"}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for ip, desc in sorted(eps.items(), key=lambda x: x[1]):
 | 
					        for ip, desc in sorted(eps.items(), key=lambda x: x[1]):
 | 
				
			||||||
            self.log(
 | 
					            self.log(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -13,7 +13,7 @@ import threading
 | 
				
			|||||||
from copy import deepcopy
 | 
					from copy import deepcopy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .__init__ import WINDOWS
 | 
					from .__init__ import WINDOWS
 | 
				
			||||||
from .util import Pebkac, Queue, fsenc
 | 
					from .util import Pebkac, Queue, fsenc, sanitize_fn
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Up2k(object):
 | 
					class Up2k(object):
 | 
				
			||||||
@@ -48,6 +48,7 @@ class Up2k(object):
 | 
				
			|||||||
        self.r_hash = re.compile("^[0-9a-zA-Z_-]{43}$")
 | 
					        self.r_hash = re.compile("^[0-9a-zA-Z_-]{43}$")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def handle_json(self, cj):
 | 
					    def handle_json(self, cj):
 | 
				
			||||||
 | 
					        cj["name"] = sanitize_fn(cj["name"])
 | 
				
			||||||
        wark = self._get_wark(cj)
 | 
					        wark = self._get_wark(cj)
 | 
				
			||||||
        now = time.time()
 | 
					        now = time.time()
 | 
				
			||||||
        with self.mutex:
 | 
					        with self.mutex:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,6 +3,7 @@ from __future__ import print_function, unicode_literals
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import re
 | 
					import re
 | 
				
			||||||
import sys
 | 
					import sys
 | 
				
			||||||
 | 
					import time
 | 
				
			||||||
import base64
 | 
					import base64
 | 
				
			||||||
import struct
 | 
					import struct
 | 
				
			||||||
import hashlib
 | 
					import hashlib
 | 
				
			||||||
@@ -42,6 +43,7 @@ if WINDOWS and PY2:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
HTTPCODE = {
 | 
					HTTPCODE = {
 | 
				
			||||||
    200: "OK",
 | 
					    200: "OK",
 | 
				
			||||||
 | 
					    204: "No Content",
 | 
				
			||||||
    206: "Partial Content",
 | 
					    206: "Partial Content",
 | 
				
			||||||
    304: "Not Modified",
 | 
					    304: "Not Modified",
 | 
				
			||||||
    400: "Bad Request",
 | 
					    400: "Bad Request",
 | 
				
			||||||
@@ -49,6 +51,7 @@ HTTPCODE = {
 | 
				
			|||||||
    404: "Not Found",
 | 
					    404: "Not Found",
 | 
				
			||||||
    405: "Method Not Allowed",
 | 
					    405: "Method Not Allowed",
 | 
				
			||||||
    413: "Payload Too Large",
 | 
					    413: "Payload Too Large",
 | 
				
			||||||
 | 
					    416: "Requested Range Not Satisfiable",
 | 
				
			||||||
    422: "Unprocessable Entity",
 | 
					    422: "Unprocessable Entity",
 | 
				
			||||||
    500: "Internal Server Error",
 | 
					    500: "Internal Server Error",
 | 
				
			||||||
    501: "Not Implemented",
 | 
					    501: "Not Implemented",
 | 
				
			||||||
@@ -309,18 +312,7 @@ def get_boundary(headers):
 | 
				
			|||||||
def read_header(sr):
 | 
					def read_header(sr):
 | 
				
			||||||
    ret = b""
 | 
					    ret = b""
 | 
				
			||||||
    while True:
 | 
					    while True:
 | 
				
			||||||
        if ret.endswith(b"\r\n\r\n"):
 | 
					        buf = sr.recv(1024)
 | 
				
			||||||
            break
 | 
					 | 
				
			||||||
        elif ret.endswith(b"\r\n\r"):
 | 
					 | 
				
			||||||
            n = 1
 | 
					 | 
				
			||||||
        elif ret.endswith(b"\r\n"):
 | 
					 | 
				
			||||||
            n = 2
 | 
					 | 
				
			||||||
        elif ret.endswith(b"\r"):
 | 
					 | 
				
			||||||
            n = 3
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            n = 4
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        buf = sr.recv(n)
 | 
					 | 
				
			||||||
        if not buf:
 | 
					        if not buf:
 | 
				
			||||||
            if not ret:
 | 
					            if not ret:
 | 
				
			||||||
                return None
 | 
					                return None
 | 
				
			||||||
@@ -332,11 +324,40 @@ def read_header(sr):
 | 
				
			|||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        ret += buf
 | 
					        ret += buf
 | 
				
			||||||
 | 
					        ofs = ret.find(b"\r\n\r\n")
 | 
				
			||||||
 | 
					        if ofs < 0:
 | 
				
			||||||
 | 
					            if len(ret) > 1024 * 64:
 | 
				
			||||||
 | 
					                raise Pebkac(400, "header 2big")
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if len(ret) > 1024 * 64:
 | 
					        sr.unrecv(ret[ofs + 4 :])
 | 
				
			||||||
            raise Pebkac(400, "header 2big")
 | 
					        return ret[:ofs].decode("utf-8", "surrogateescape").split("\r\n")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return ret[:-4].decode("utf-8", "surrogateescape").split("\r\n")
 | 
					
 | 
				
			||||||
 | 
					def humansize(sz, terse=False):
 | 
				
			||||||
 | 
					    for unit in ["B", "KiB", "MiB", "GiB", "TiB"]:
 | 
				
			||||||
 | 
					        if sz < 1024:
 | 
				
			||||||
 | 
					            break
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        sz /= 1024.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ret = " ".join([str(sz)[:4].rstrip("."), unit])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if not terse:
 | 
				
			||||||
 | 
					        return ret
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return ret.replace("iB", "").replace(" ", "")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_spd(nbyte, t0, t=None):
 | 
				
			||||||
 | 
					    if t is None:
 | 
				
			||||||
 | 
					        t = time.time()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    bps = nbyte / ((t - t0) + 0.001)
 | 
				
			||||||
 | 
					    s1 = humansize(nbyte).replace(" ", "\033[33m").replace("iB", "")
 | 
				
			||||||
 | 
					    s2 = humansize(bps).replace(" ", "\033[35m").replace("iB", "")
 | 
				
			||||||
 | 
					    return "{} \033[0m{}/s\033[0m".format(s1, s2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def undot(path):
 | 
					def undot(path):
 | 
				
			||||||
@@ -356,7 +377,30 @@ def undot(path):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def sanitize_fn(fn):
 | 
					def sanitize_fn(fn):
 | 
				
			||||||
    return fn.replace("\\", "/").split("/")[-1].strip()
 | 
					    fn = fn.replace("\\", "/").split("/")[-1]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if WINDOWS:
 | 
				
			||||||
 | 
					        for bad, good in [
 | 
				
			||||||
 | 
					            ["<", "<"],
 | 
				
			||||||
 | 
					            [">", ">"],
 | 
				
			||||||
 | 
					            [":", ":"],
 | 
				
			||||||
 | 
					            ['"', """],
 | 
				
			||||||
 | 
					            ["/", "/"],
 | 
				
			||||||
 | 
					            ["\\", "\"],
 | 
				
			||||||
 | 
					            ["|", "|"],
 | 
				
			||||||
 | 
					            ["?", "?"],
 | 
				
			||||||
 | 
					            ["*", "*"],
 | 
				
			||||||
 | 
					        ]:
 | 
				
			||||||
 | 
					            fn = fn.replace(bad, good)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        bad = ["con", "prn", "aux", "nul"]
 | 
				
			||||||
 | 
					        for n in range(1, 10):
 | 
				
			||||||
 | 
					            bad += "com{0} lpt{0}".format(n).split(" ")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if fn.lower() in bad:
 | 
				
			||||||
 | 
					            fn = "_" + fn
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return fn.strip()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def exclude_dotfiles(filepaths):
 | 
					def exclude_dotfiles(filepaths):
 | 
				
			||||||
@@ -365,6 +409,21 @@ def exclude_dotfiles(filepaths):
 | 
				
			|||||||
            yield fpath
 | 
					            yield fpath
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def html_escape(s, quote=False):
 | 
				
			||||||
 | 
					    """html.escape but also newlines"""
 | 
				
			||||||
 | 
					    s = (
 | 
				
			||||||
 | 
					        s.replace("&", "&")
 | 
				
			||||||
 | 
					        .replace("<", "<")
 | 
				
			||||||
 | 
					        .replace(">", ">")
 | 
				
			||||||
 | 
					        .replace("\r", "
")
 | 
				
			||||||
 | 
					        .replace("\n", "
")
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    if quote:
 | 
				
			||||||
 | 
					        s = s.replace('"', """).replace("'", "'")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return s
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def quotep(txt):
 | 
					def quotep(txt):
 | 
				
			||||||
    """url quoter which deals with bytes correctly"""
 | 
					    """url quoter which deals with bytes correctly"""
 | 
				
			||||||
    btxt = w8enc(txt)
 | 
					    btxt = w8enc(txt)
 | 
				
			||||||
@@ -379,8 +438,8 @@ def quotep(txt):
 | 
				
			|||||||
def unquotep(txt):
 | 
					def unquotep(txt):
 | 
				
			||||||
    """url unquoter which deals with bytes correctly"""
 | 
					    """url unquoter which deals with bytes correctly"""
 | 
				
			||||||
    btxt = w8enc(txt)
 | 
					    btxt = w8enc(txt)
 | 
				
			||||||
    unq1 = btxt.replace(b"+", b" ")
 | 
					    # btxt = btxt.replace(b"+", b" ")
 | 
				
			||||||
    unq2 = unquote(unq1)
 | 
					    unq2 = unquote(btxt)
 | 
				
			||||||
    return w8dec(unq2)
 | 
					    return w8dec(unq2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -428,6 +487,15 @@ def read_socket(sr, total_size):
 | 
				
			|||||||
        yield buf
 | 
					        yield buf
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def read_socket_unbounded(sr):
 | 
				
			||||||
 | 
					    while True:
 | 
				
			||||||
 | 
					        buf = sr.recv(32 * 1024)
 | 
				
			||||||
 | 
					        if not buf:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        yield buf
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def hashcopy(actor, fin, fout):
 | 
					def hashcopy(actor, fin, fout):
 | 
				
			||||||
    u32_lim = int((2 ** 31) * 0.9)
 | 
					    u32_lim = int((2 ** 31) * 0.9)
 | 
				
			||||||
    hashobj = hashlib.sha512()
 | 
					    hashobj = hashlib.sha512()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -68,7 +68,7 @@ a {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
#files thead th:last-child {
 | 
					#files thead th:last-child {
 | 
				
			||||||
	background: #444;
 | 
						background: #444;
 | 
				
			||||||
	border-radius: .7em 0 0 0;
 | 
						border-radius: .7em .7em 0 0;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
#files thead th:first-child {
 | 
					#files thead th:first-child {
 | 
				
			||||||
	background: #222;
 | 
						background: #222;
 | 
				
			||||||
@@ -131,6 +131,17 @@ a {
 | 
				
			|||||||
.logue {
 | 
					.logue {
 | 
				
			||||||
	padding: .2em 1.5em;
 | 
						padding: .2em 1.5em;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					#srv_info {
 | 
				
			||||||
 | 
						opacity: .5;
 | 
				
			||||||
 | 
						font-size: .8em;
 | 
				
			||||||
 | 
						color: #fc5;
 | 
				
			||||||
 | 
						position: absolute;
 | 
				
			||||||
 | 
						top: .5em;
 | 
				
			||||||
 | 
						left: 2em;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					#srv_info span {
 | 
				
			||||||
 | 
						color: #fff;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
a.play {
 | 
					a.play {
 | 
				
			||||||
	color: #e70;
 | 
						color: #e70;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -33,7 +33,7 @@
 | 
				
			|||||||
            <tr>
 | 
					            <tr>
 | 
				
			||||||
                <th></th>
 | 
					                <th></th>
 | 
				
			||||||
                <th>File Name</th>
 | 
					                <th>File Name</th>
 | 
				
			||||||
                <th>File Size</th>
 | 
					                <th sort="int">File Size</th>
 | 
				
			||||||
                <th>Date</th>
 | 
					                <th>Date</th>
 | 
				
			||||||
            </tr>
 | 
					            </tr>
 | 
				
			||||||
        </thead>
 | 
					        </thead>
 | 
				
			||||||
@@ -53,6 +53,10 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    <h2><a href="?h">control-panel</a></h2>
 | 
					    <h2><a href="?h">control-panel</a></h2>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    {%- if srv_info %}
 | 
				
			||||||
 | 
					    <div id="srv_info"><span>{{ srv_info }}</span></div>
 | 
				
			||||||
 | 
					    {%- endif %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div id="widget">
 | 
					    <div id="widget">
 | 
				
			||||||
        <div id="wtoggle">♫</div>
 | 
					        <div id="wtoggle">♫</div>
 | 
				
			||||||
        <div id="widgeti">
 | 
					        <div id="widgeti">
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -34,6 +34,7 @@ window.onerror = function (msg, url, lineNo, columnNo, error) {
 | 
				
			|||||||
					esc(String(error[find[a]])).replace(/\n/g, '<br />\n'));
 | 
										esc(String(error[find[a]])).replace(/\n/g, '<br />\n'));
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	document.body.style.fontSize = '0.8em';
 | 
						document.body.style.fontSize = '0.8em';
 | 
				
			||||||
 | 
						document.body.style.padding = '0 1em 1em 1em';
 | 
				
			||||||
	hcroak(html.join('\n'));
 | 
						hcroak(html.join('\n'));
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -78,6 +79,41 @@ function ev(e) {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function sortTable(table, col) {
 | 
				
			||||||
 | 
						var tb = table.tBodies[0], // use `<tbody>` to ignore `<thead>` and `<tfoot>` rows
 | 
				
			||||||
 | 
							th = table.tHead.rows[0].cells,
 | 
				
			||||||
 | 
							tr = Array.prototype.slice.call(tb.rows, 0),
 | 
				
			||||||
 | 
							i, reverse = th[col].className == 'sort1' ? -1 : 1;
 | 
				
			||||||
 | 
						for (var a = 0, thl = th.length; a < thl; a++)
 | 
				
			||||||
 | 
							th[a].className = '';
 | 
				
			||||||
 | 
						th[col].className = 'sort' + reverse;
 | 
				
			||||||
 | 
						var stype = th[col].getAttribute('sort');
 | 
				
			||||||
 | 
						tr = tr.sort(function (a, b) {
 | 
				
			||||||
 | 
							var v1 = a.cells[col].textContent.trim();
 | 
				
			||||||
 | 
							var v2 = b.cells[col].textContent.trim();
 | 
				
			||||||
 | 
							if (stype == 'int') {
 | 
				
			||||||
 | 
								v1 = parseInt(v1.replace(/,/g, ''));
 | 
				
			||||||
 | 
								v2 = parseInt(v2.replace(/,/g, ''));
 | 
				
			||||||
 | 
								return reverse * (v1 - v2);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return reverse * (v1.localeCompare(v2));
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
						for (i = 0; i < tr.length; ++i) tb.appendChild(tr[i]);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					function makeSortable(table) {
 | 
				
			||||||
 | 
						var th = table.tHead, i;
 | 
				
			||||||
 | 
						th && (th = th.rows[0]) && (th = th.cells);
 | 
				
			||||||
 | 
						if (th) i = th.length;
 | 
				
			||||||
 | 
						else return; // if no `<thead>` then do nothing
 | 
				
			||||||
 | 
						while (--i >= 0) (function (i) {
 | 
				
			||||||
 | 
							th[i].onclick = function () {
 | 
				
			||||||
 | 
								sortTable(table, i);
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						}(i));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					makeSortable(o('files'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// extract songs + add play column
 | 
					// extract songs + add play column
 | 
				
			||||||
var mp = (function () {
 | 
					var mp = (function () {
 | 
				
			||||||
	var tracks = [];
 | 
						var tracks = [];
 | 
				
			||||||
@@ -89,7 +125,6 @@ var mp = (function () {
 | 
				
			|||||||
		'cover_url': ''
 | 
							'cover_url': ''
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
	var re_audio = new RegExp('\.(opus|ogg|m4a|aac|mp3|wav|flac)$', 'i');
 | 
						var re_audio = new RegExp('\.(opus|ogg|m4a|aac|mp3|wav|flac)$', 'i');
 | 
				
			||||||
	var re_cover = new RegExp('^(cover|folder|cd|front|back)\.(jpe?g|png|gif)$', 'i');
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	var trs = document.getElementById('files').getElementsByTagName('tbody')[0].getElementsByTagName('tr');
 | 
						var trs = document.getElementById('files').getElementsByTagName('tbody')[0].getElementsByTagName('tr');
 | 
				
			||||||
	for (var a = 0, aa = trs.length; a < aa; a++) {
 | 
						for (var a = 0, aa = trs.length; a < aa; a++) {
 | 
				
			||||||
@@ -380,15 +415,6 @@ var vbar = (function () {
 | 
				
			|||||||
		var x = e.clientX - rect.left;
 | 
							var x = e.clientX - rect.left;
 | 
				
			||||||
		var mul = x * 1.0 / rect.width;
 | 
							var mul = x * 1.0 / rect.width;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		/*
 | 
					 | 
				
			||||||
		dbg(//Math.round(rect.width) + 'x' + Math.round(rect.height) + '+' +
 | 
					 | 
				
			||||||
			//Math.round(rect.left) + '+' + Math.round(rect.top) + ', ' +
 | 
					 | 
				
			||||||
			//Math.round(e.clientX) + 'x' + Math.round(e.clientY) + ', ' +
 | 
					 | 
				
			||||||
			Math.round(mp.au.currentTime * 10) / 10 + ', ' +
 | 
					 | 
				
			||||||
			Math.round(mp.au.duration * 10) / 10 + '*' +
 | 
					 | 
				
			||||||
			Math.round(mul * 1000) / 1000);
 | 
					 | 
				
			||||||
		*/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		mp.au.currentTime = mp.au.duration * mul;
 | 
							mp.au.currentTime = mp.au.duration * mul;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (mp.au === mp.au_native)
 | 
							if (mp.au === mp.au_native)
 | 
				
			||||||
@@ -449,8 +475,14 @@ function setclass(id, clas) {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var iOS = !!navigator.platform &&
 | 
					var need_ogv = true;
 | 
				
			||||||
	/iPad|iPhone|iPod/.test(navigator.platform);
 | 
					try {
 | 
				
			||||||
 | 
						need_ogv = new Audio().canPlayType('audio/ogg; codecs=opus') !== 'probably';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (/ Edge\//.exec(navigator.userAgent + ''))
 | 
				
			||||||
 | 
							need_ogv = true;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					catch (ex) { }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// plays the tid'th audio file on the page
 | 
					// plays the tid'th audio file on the page
 | 
				
			||||||
@@ -473,7 +505,7 @@ function play(tid, call_depth) {
 | 
				
			|||||||
	var hack_attempt_play = true;
 | 
						var hack_attempt_play = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	var url = mp.tracks[tid];
 | 
						var url = mp.tracks[tid];
 | 
				
			||||||
	if (iOS && /\.(ogg|opus)$/i.test(url)) {
 | 
						if (need_ogv && /\.(ogg|opus)$/i.test(url)) {
 | 
				
			||||||
		if (mp.au_ogvjs) {
 | 
							if (mp.au_ogvjs) {
 | 
				
			||||||
			mp.au = mp.au_ogvjs;
 | 
								mp.au = mp.au_ogvjs;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@@ -560,7 +592,6 @@ function evau_error(e) {
 | 
				
			|||||||
	err += '\n\nFile: «' + decodeURIComponent(eplaya.src.split('/').slice(-1)[0]) + '»';
 | 
						err += '\n\nFile: «' + decodeURIComponent(eplaya.src.split('/').slice(-1)[0]) + '»';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	alert(err);
 | 
						alert(err);
 | 
				
			||||||
	play(eplaya.tid + 1);
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -579,7 +610,7 @@ function show_modal(html) {
 | 
				
			|||||||
function unblocked() {
 | 
					function unblocked() {
 | 
				
			||||||
	var dom = o('blocked');
 | 
						var dom = o('blocked');
 | 
				
			||||||
	if (dom)
 | 
						if (dom)
 | 
				
			||||||
		dom.remove();
 | 
							dom.parentNode.removeChild(dom);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,13 +1,19 @@
 | 
				
			|||||||
 | 
					@font-face {
 | 
				
			||||||
 | 
						font-family: 'scp';
 | 
				
			||||||
 | 
						src: local('Source Code Pro Regular'), local('SourceCodePro-Regular'), url(/.cpr/deps/scp.woff2) format('woff2');
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
html, body {
 | 
					html, body {
 | 
				
			||||||
	color: #333;
 | 
						color: #333;
 | 
				
			||||||
	background: #eee;
 | 
						background: #eee;
 | 
				
			||||||
	font-family: sans-serif;
 | 
						font-family: sans-serif;
 | 
				
			||||||
	line-height: 1.5em;
 | 
						line-height: 1.5em;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					#mtw {
 | 
				
			||||||
 | 
						display: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
#mw {
 | 
					#mw {
 | 
				
			||||||
	width: 48.5em;
 | 
					 | 
				
			||||||
	margin: 0 auto;
 | 
						margin: 0 auto;
 | 
				
			||||||
	margin-bottom: 6em;
 | 
						padding: 0 1.5em;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
pre, code, a {
 | 
					pre, code, a {
 | 
				
			||||||
	color: #480;
 | 
						color: #480;
 | 
				
			||||||
@@ -21,7 +27,7 @@ code {
 | 
				
			|||||||
	font-size: .96em;
 | 
						font-size: .96em;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
pre, code {
 | 
					pre, code {
 | 
				
			||||||
	font-family: monospace, monospace;
 | 
						font-family: 'scp', monospace, monospace;
 | 
				
			||||||
	white-space: pre-wrap;
 | 
						white-space: pre-wrap;
 | 
				
			||||||
	word-break: break-all;
 | 
						word-break: break-all;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -41,7 +47,7 @@ pre code {
 | 
				
			|||||||
pre code:last-child {
 | 
					pre code:last-child {
 | 
				
			||||||
	border-bottom: none;
 | 
						border-bottom: none;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
pre code:before {
 | 
					pre code::before {
 | 
				
			||||||
	content: counter(precode);
 | 
						content: counter(precode);
 | 
				
			||||||
	-webkit-user-select: none;
 | 
						-webkit-user-select: none;
 | 
				
			||||||
	display: inline-block;
 | 
						display: inline-block;
 | 
				
			||||||
@@ -76,31 +82,39 @@ h2 {
 | 
				
			|||||||
	padding-left: .4em;
 | 
						padding-left: .4em;
 | 
				
			||||||
	margin-top: 3em;
 | 
						margin-top: 3em;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					h3 {
 | 
				
			||||||
 | 
						border-bottom: .1em solid #999;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
h1 a, h3 a, h5 a,
 | 
					h1 a, h3 a, h5 a,
 | 
				
			||||||
h2 a, h4 a, h6 a {
 | 
					h2 a, h4 a, h6 a {
 | 
				
			||||||
	color: inherit;
 | 
						color: inherit;
 | 
				
			||||||
 | 
						display: block;
 | 
				
			||||||
	background: none;
 | 
						background: none;
 | 
				
			||||||
	border: none;
 | 
						border: none;
 | 
				
			||||||
	padding: 0;
 | 
						padding: 0;
 | 
				
			||||||
	margin: 0;
 | 
						margin: 0;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
#m ul,
 | 
					#mp ul,
 | 
				
			||||||
#m ol {
 | 
					#mp ol {
 | 
				
			||||||
	border-left: .3em solid #ddd;
 | 
						border-left: .3em solid #ddd;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
#m>ul,
 | 
					#m>ul,
 | 
				
			||||||
#m>ol {
 | 
					#m>ol {
 | 
				
			||||||
	border-color: #bbb;
 | 
						border-color: #bbb;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
#m ul>li {
 | 
					#mp ul>li {
 | 
				
			||||||
	list-style-type: disc;
 | 
						list-style-type: disc;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
#m ul>li,
 | 
					#mp ul>li,
 | 
				
			||||||
#m ol>li {
 | 
					#mp ol>li {
 | 
				
			||||||
	margin: .7em 0;
 | 
						margin: .7em 0;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					strong {
 | 
				
			||||||
 | 
						color: #000;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
p>em,
 | 
					p>em,
 | 
				
			||||||
li>em {
 | 
					li>em,
 | 
				
			||||||
 | 
					td>em {
 | 
				
			||||||
	color: #c50;
 | 
						color: #c50;
 | 
				
			||||||
	padding: .1em;
 | 
						padding: .1em;
 | 
				
			||||||
	border-bottom: .1em solid #bbb;
 | 
						border-bottom: .1em solid #bbb;
 | 
				
			||||||
@@ -116,8 +130,9 @@ small {
 | 
				
			|||||||
	opacity: .8;
 | 
						opacity: .8;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
#toc {
 | 
					#toc {
 | 
				
			||||||
	width: 48.5em;
 | 
						margin: 0 1em;
 | 
				
			||||||
	margin: 0 auto;
 | 
						-ms-scroll-chaining: none;
 | 
				
			||||||
 | 
						overscroll-behavior-y: none;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
#toc ul {
 | 
					#toc ul {
 | 
				
			||||||
	padding-left: 1em;
 | 
						padding-left: 1em;
 | 
				
			||||||
@@ -162,14 +177,12 @@ small {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
table {
 | 
					table {
 | 
				
			||||||
	border-collapse: collapse;
 | 
						border-collapse: collapse;
 | 
				
			||||||
 | 
						margin: 1em 0;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
td {
 | 
					th, td {
 | 
				
			||||||
	padding: .2em .5em;
 | 
						padding: .2em .5em;
 | 
				
			||||||
	border: .12em solid #aaa;
 | 
						border: .12em solid #aaa;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
th {
 | 
					 | 
				
			||||||
	border: .12em solid #aaa;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
blink {
 | 
					blink {
 | 
				
			||||||
	animation: blinker .7s cubic-bezier(.9, 0, .1, 1) infinite;
 | 
						animation: blinker .7s cubic-bezier(.9, 0, .1, 1) infinite;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -181,10 +194,26 @@ blink {
 | 
				
			|||||||
		opacity: 1;
 | 
							opacity: 1;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@media screen {
 | 
					@media screen {
 | 
				
			||||||
	html, body {
 | 
						html, body {
 | 
				
			||||||
		margin: 0;
 | 
							margin: 0;
 | 
				
			||||||
		padding: 0;
 | 
							padding: 0;
 | 
				
			||||||
 | 
							outline: 0;
 | 
				
			||||||
 | 
							border: none;
 | 
				
			||||||
 | 
							width: 100%;
 | 
				
			||||||
 | 
							height: 100%;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						#mw {
 | 
				
			||||||
 | 
							margin: 0 auto;
 | 
				
			||||||
 | 
							right: 0;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						#mp {
 | 
				
			||||||
 | 
							max-width: 52em;
 | 
				
			||||||
 | 
							margin-bottom: 6em;
 | 
				
			||||||
 | 
							word-break: break-word;
 | 
				
			||||||
 | 
							overflow-wrap: break-word;
 | 
				
			||||||
 | 
							word-wrap: break-word; /*ie*/
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	a {
 | 
						a {
 | 
				
			||||||
		color: #fff;
 | 
							color: #fff;
 | 
				
			||||||
@@ -212,15 +241,17 @@ blink {
 | 
				
			|||||||
		padding: .5em 0;
 | 
							padding: .5em 0;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	#mn {
 | 
						#mn {
 | 
				
			||||||
		font-weight: normal;
 | 
					 | 
				
			||||||
		padding: 1.3em 0 .7em 1em;
 | 
							padding: 1.3em 0 .7em 1em;
 | 
				
			||||||
		font-size: 1.4em;
 | 
							border-bottom: 1px solid #ccc;
 | 
				
			||||||
 | 
							background: #eee;
 | 
				
			||||||
 | 
							z-index: 10;
 | 
				
			||||||
 | 
							width: calc(100% - 1em);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	#mn a {
 | 
						#mn a {
 | 
				
			||||||
		color: #444;
 | 
							color: #444;
 | 
				
			||||||
		background: none;
 | 
							background: none;
 | 
				
			||||||
		margin: 0 0 0 -.2em;
 | 
							margin: 0 0 0 -.2em;
 | 
				
			||||||
		padding: 0 0 0 .4em;
 | 
							padding: .3em 0 .3em .4em;
 | 
				
			||||||
		text-decoration: none;
 | 
							text-decoration: none;
 | 
				
			||||||
		border: none;
 | 
							border: none;
 | 
				
			||||||
		/* ie: */
 | 
							/* ie: */
 | 
				
			||||||
@@ -233,14 +264,14 @@ blink {
 | 
				
			|||||||
	#mn a:last-child {
 | 
						#mn a:last-child {
 | 
				
			||||||
		padding-right: .5em;
 | 
							padding-right: .5em;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	#mn a:not(:last-child):after {
 | 
						#mn a:not(:last-child)::after {
 | 
				
			||||||
		content: '';
 | 
							content: '';
 | 
				
			||||||
		width: 1.05em;
 | 
							width: 1.05em;
 | 
				
			||||||
		height: 1.05em;
 | 
							height: 1.05em;
 | 
				
			||||||
		margin: -.2em .3em -.2em -.4em;
 | 
							margin: -.2em .3em -.2em -.4em;
 | 
				
			||||||
		display: inline-block;
 | 
							display: inline-block;
 | 
				
			||||||
		border: 1px solid rgba(0,0,0,0.3);
 | 
							border: 1px solid rgba(0,0,0,0.2);
 | 
				
			||||||
		border-width: .05em .05em 0 0;
 | 
							border-width: .2em .2em 0 0;
 | 
				
			||||||
		transform: rotate(45deg);
 | 
							transform: rotate(45deg);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	#mn a:hover {
 | 
						#mn a:hover {
 | 
				
			||||||
@@ -248,7 +279,45 @@ blink {
 | 
				
			|||||||
		text-decoration: underline;
 | 
							text-decoration: underline;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	#mh {
 | 
						#mh {
 | 
				
			||||||
		margin: 0 0 1.5em 0;
 | 
							padding: .4em 1em;
 | 
				
			||||||
 | 
							position: relative;
 | 
				
			||||||
 | 
							width: 100%;
 | 
				
			||||||
 | 
							width: calc(100% - 3em);
 | 
				
			||||||
 | 
							background: #eee;
 | 
				
			||||||
 | 
							z-index: 9;
 | 
				
			||||||
 | 
							top: 0;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						#mh a {
 | 
				
			||||||
 | 
							color: #444;
 | 
				
			||||||
 | 
							background: none;
 | 
				
			||||||
 | 
							text-decoration: underline;
 | 
				
			||||||
 | 
							border: none;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						#mh a:hover {
 | 
				
			||||||
 | 
							color: #000;
 | 
				
			||||||
 | 
							background: #ddd;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						#toolsbox {
 | 
				
			||||||
 | 
							overflow: hidden;
 | 
				
			||||||
 | 
							display: inline-block;
 | 
				
			||||||
 | 
							background: #eee;
 | 
				
			||||||
 | 
							height: 1.5em;
 | 
				
			||||||
 | 
							padding: 0 .2em;
 | 
				
			||||||
 | 
							margin: 0 .2em;
 | 
				
			||||||
 | 
							position: absolute;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						#toolsbox.open {
 | 
				
			||||||
 | 
							height: auto;
 | 
				
			||||||
 | 
							overflow: visible;
 | 
				
			||||||
 | 
							background: #eee;
 | 
				
			||||||
 | 
							box-shadow: 0 .2em .2em #ccc;
 | 
				
			||||||
 | 
							padding-bottom: .2em;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						#toolsbox a {
 | 
				
			||||||
 | 
							display: block;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						#toolsbox a+a {
 | 
				
			||||||
 | 
							text-decoration: none;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -270,13 +339,12 @@ blink {
 | 
				
			|||||||
	html.dark #toc li {
 | 
						html.dark #toc li {
 | 
				
			||||||
		border-width: 0;
 | 
							border-width: 0;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	html.dark #m a,
 | 
						html.dark #mp a {
 | 
				
			||||||
	html.dark #mh a {
 | 
					 | 
				
			||||||
		background: #057;
 | 
							background: #057;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	html.dark #m h1 a, html.dark #m h4 a,
 | 
						html.dark #mp h1 a, html.dark #mp h4 a,
 | 
				
			||||||
	html.dark #m h2 a, html.dark #m h5 a,
 | 
						html.dark #mp h2 a, html.dark #mp h5 a,
 | 
				
			||||||
	html.dark #m h3 a, html.dark #m h6 a {
 | 
						html.dark #mp h3 a, html.dark #mp h6 a {
 | 
				
			||||||
		color: inherit;
 | 
							color: inherit;
 | 
				
			||||||
		background: none;
 | 
							background: none;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -286,16 +354,20 @@ blink {
 | 
				
			|||||||
		background: #1a1a1a;
 | 
							background: #1a1a1a;
 | 
				
			||||||
		border: .07em solid #333;
 | 
							border: .07em solid #333;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	html.dark #m ul,
 | 
						html.dark #mp ul,
 | 
				
			||||||
	html.dark #m ol {
 | 
						html.dark #mp ol {
 | 
				
			||||||
		border-color: #444;
 | 
							border-color: #444;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	html.dark #m>ul,
 | 
						html.dark #m>ul,
 | 
				
			||||||
	html.dark #m>ol {
 | 
						html.dark #m>ol {
 | 
				
			||||||
		border-color: #555;
 | 
							border-color: #555;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						html.dark strong {
 | 
				
			||||||
 | 
							color: #fff;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	html.dark p>em,
 | 
						html.dark p>em,
 | 
				
			||||||
	html.dark li>em {
 | 
						html.dark li>em,
 | 
				
			||||||
 | 
						html.dark td>em {
 | 
				
			||||||
		color: #f94;
 | 
							color: #f94;
 | 
				
			||||||
		border-color: #666;
 | 
							border-color: #666;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -316,32 +388,61 @@ blink {
 | 
				
			|||||||
		background: #282828;
 | 
							background: #282828;
 | 
				
			||||||
		border: .07em dashed #444;
 | 
							border: .07em dashed #444;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	html.dark #mn a:not(:last-child):after {
 | 
						html.dark #mn a:not(:last-child)::after {
 | 
				
			||||||
		border-color: rgba(255,255,255,0.3);
 | 
							border-color: rgba(255,255,255,0.3);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	html.dark #mn a {
 | 
						html.dark #mn a {
 | 
				
			||||||
		color: #ccc;
 | 
							color: #ccc;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						html.dark #mn {
 | 
				
			||||||
 | 
							border-bottom: 1px solid #333;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						html.dark #mn,
 | 
				
			||||||
 | 
						html.dark #mh {
 | 
				
			||||||
 | 
							background: #222;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						html.dark #mh a {
 | 
				
			||||||
 | 
							color: #ccc;
 | 
				
			||||||
 | 
							background: none;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						html.dark #mh a:hover {
 | 
				
			||||||
 | 
							background: #333;
 | 
				
			||||||
 | 
							color: #fff;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						html.dark #toolsbox {
 | 
				
			||||||
 | 
							background: #222;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						html.dark #toolsbox.open {
 | 
				
			||||||
 | 
							box-shadow: 0 .2em .2em #069;
 | 
				
			||||||
 | 
							border-radius: 0 0 .4em .4em;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@media screen and (min-width: 64em) {
 | 
					
 | 
				
			||||||
 | 
					@media screen and (min-width: 66em) {
 | 
				
			||||||
	#mw {
 | 
						#mw {
 | 
				
			||||||
		margin-left: 14em;
 | 
							position: fixed;
 | 
				
			||||||
		margin-left: calc(100% - 50em);
 | 
							overflow-y: auto;
 | 
				
			||||||
 | 
							left: 14em;
 | 
				
			||||||
 | 
							left: calc(100% - 55em);
 | 
				
			||||||
 | 
							max-width: none;
 | 
				
			||||||
 | 
							bottom: 0;
 | 
				
			||||||
 | 
							scrollbar-color: #eb0 #f7f7f7;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	#toc {
 | 
						#toc {
 | 
				
			||||||
		width: 13em;
 | 
							width: 13em;
 | 
				
			||||||
		width: calc(100% - 52.3em);
 | 
							width: calc(100% - 55.3em);
 | 
				
			||||||
 | 
							max-width: 30em;
 | 
				
			||||||
		background: #eee;
 | 
							background: #eee;
 | 
				
			||||||
		position: fixed;
 | 
							position: fixed;
 | 
				
			||||||
 | 
							overflow-y: auto;
 | 
				
			||||||
		top: 0;
 | 
							top: 0;
 | 
				
			||||||
		left: 0;
 | 
							left: 0;
 | 
				
			||||||
		height: 100%;
 | 
							bottom: 0;
 | 
				
			||||||
		overflow-y: auto;
 | 
					 | 
				
			||||||
		padding: 0;
 | 
							padding: 0;
 | 
				
			||||||
		margin: 0;
 | 
							margin: 0;
 | 
				
			||||||
		box-shadow: 0 0 1em #ccc;
 | 
					 | 
				
			||||||
		scrollbar-color: #eb0 #f7f7f7;
 | 
							scrollbar-color: #eb0 #f7f7f7;
 | 
				
			||||||
		xscrollbar-width: thin;
 | 
							box-shadow: 0 0 1em rgba(0,0,0,0.1);
 | 
				
			||||||
 | 
							border-top: 1px solid #d7d7d7;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	#toc li {
 | 
						#toc li {
 | 
				
			||||||
		border-left: .3em solid #ccc;
 | 
							border-left: .3em solid #ccc;
 | 
				
			||||||
@@ -361,30 +462,134 @@ blink {
 | 
				
			|||||||
	
 | 
						
 | 
				
			||||||
	html.dark #toc {
 | 
						html.dark #toc {
 | 
				
			||||||
		background: #282828;
 | 
							background: #282828;
 | 
				
			||||||
 | 
							border-top: 1px solid #2c2c2c;
 | 
				
			||||||
		box-shadow: 0 0 1em #181818;
 | 
							box-shadow: 0 0 1em #181818;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						html.dark #toc,
 | 
				
			||||||
 | 
						html.dark #mw {
 | 
				
			||||||
		scrollbar-color: #b80 #282828;
 | 
							scrollbar-color: #b80 #282828;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						html.dark #toc::-webkit-scrollbar-track {
 | 
				
			||||||
 | 
							background: #282828;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						html.dark #toc::-webkit-scrollbar {
 | 
				
			||||||
 | 
							background: #282828;
 | 
				
			||||||
 | 
							width: .8em;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						html.dark #toc::-webkit-scrollbar-thumb {
 | 
				
			||||||
 | 
							background: #b80;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@media screen and (min-width: 84em) {
 | 
					@media screen and (min-width: 85.5em) {
 | 
				
			||||||
	#toc { width: 30em }
 | 
						#toc { width: 30em }
 | 
				
			||||||
	#mw { margin-left: 32em }
 | 
						#mw { left: 30.5em }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@media print {
 | 
					@media print {
 | 
				
			||||||
 | 
						@page {
 | 
				
			||||||
 | 
							size: A4;
 | 
				
			||||||
 | 
							padding: 0;
 | 
				
			||||||
 | 
							margin: .5in .6in;
 | 
				
			||||||
 | 
							mso-header-margin: .6in;
 | 
				
			||||||
 | 
							mso-footer-margin: .6in;
 | 
				
			||||||
 | 
							mso-paper-source: 0;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	a {
 | 
						a {
 | 
				
			||||||
		color: #079;
 | 
							color: #079;
 | 
				
			||||||
		text-decoration: none;
 | 
							text-decoration: none;
 | 
				
			||||||
		border-bottom: .07em solid #4ac;
 | 
							border-bottom: .07em solid #4ac;
 | 
				
			||||||
		padding: 0 .3em;
 | 
							padding: 0 .3em;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	#toc {
 | 
					 | 
				
			||||||
		margin: 0 !important;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	#toc>ul {
 | 
						#toc>ul {
 | 
				
			||||||
		border-left: .1em solid #84c4dd;
 | 
							border-left: .1em solid #84c4dd;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	#mn, #mh {
 | 
						#mn, #mh {
 | 
				
			||||||
		display: none;
 | 
							display: none;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						html, body, #toc, #mw {
 | 
				
			||||||
 | 
							margin: 0 !important;
 | 
				
			||||||
 | 
							word-break: break-word;
 | 
				
			||||||
 | 
							width: 52em;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						#toc {
 | 
				
			||||||
 | 
							margin-left: 1em !important;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						#toc a {
 | 
				
			||||||
 | 
							color: #000 !important;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						#toc a::after {
 | 
				
			||||||
 | 
							/* hopefully supported by browsers eventually */
 | 
				
			||||||
 | 
							content: leader('.') target-counter(attr(href), page);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						a[ctr]::before {
 | 
				
			||||||
 | 
							content: attr(ctr) '. ';
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						h1 {
 | 
				
			||||||
 | 
							margin: 2em 0;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						h2 {
 | 
				
			||||||
 | 
							margin: 2em 0 0 0;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						h1, h2, h3 {
 | 
				
			||||||
 | 
							page-break-inside: avoid;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						h1::after,
 | 
				
			||||||
 | 
						h2::after,
 | 
				
			||||||
 | 
						h3::after {
 | 
				
			||||||
 | 
							content: 'orz';
 | 
				
			||||||
 | 
							color: transparent;
 | 
				
			||||||
 | 
							display: block;
 | 
				
			||||||
 | 
							line-height: 1em;
 | 
				
			||||||
 | 
							padding: 4em 0 0 0;
 | 
				
			||||||
 | 
							margin: 0 0 -5em 0;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						p {
 | 
				
			||||||
 | 
							page-break-inside: avoid;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						table {
 | 
				
			||||||
 | 
							page-break-inside: auto;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						tr {
 | 
				
			||||||
 | 
							page-break-inside: avoid;
 | 
				
			||||||
 | 
							page-break-after: auto;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						thead {
 | 
				
			||||||
 | 
							display: table-header-group;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						tfoot {
 | 
				
			||||||
 | 
							display: table-footer-group;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						#mp a.vis::after {
 | 
				
			||||||
 | 
							content: ' (' attr(href) ')';
 | 
				
			||||||
 | 
							border-bottom: 1px solid #bbb;
 | 
				
			||||||
 | 
							color: #444;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						blockquote {
 | 
				
			||||||
 | 
							border-color: #555;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						code {
 | 
				
			||||||
 | 
							border-color: #bbb;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						pre, pre code {
 | 
				
			||||||
 | 
							border-color: #999;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						pre code::before {
 | 
				
			||||||
 | 
							color: #058;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						html.dark a {
 | 
				
			||||||
 | 
							color: #000;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						html.dark pre,
 | 
				
			||||||
 | 
						html.dark code {
 | 
				
			||||||
 | 
							color: #240;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						html.dark p>em,
 | 
				
			||||||
 | 
						html.dark li>em,
 | 
				
			||||||
 | 
						html.dark td>em {
 | 
				
			||||||
 | 
							color: #940;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/*
 | 
					/*
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,32 +4,132 @@
 | 
				
			|||||||
	<meta http-equiv="X-UA-Compatible" content="IE=edge">
 | 
						<meta http-equiv="X-UA-Compatible" content="IE=edge">
 | 
				
			||||||
	<meta name="viewport" content="width=device-width, initial-scale=0.7">
 | 
						<meta name="viewport" content="width=device-width, initial-scale=0.7">
 | 
				
			||||||
	<link href="/.cpr/md.css" rel="stylesheet">
 | 
						<link href="/.cpr/md.css" rel="stylesheet">
 | 
				
			||||||
 | 
						{%- if edit %}
 | 
				
			||||||
 | 
						<link href="/.cpr/md2.css" rel="stylesheet">
 | 
				
			||||||
 | 
						{%- endif %}
 | 
				
			||||||
</head>
 | 
					</head>
 | 
				
			||||||
<body>
 | 
					<body>
 | 
				
			||||||
	<div id="mn"></div>
 | 
						<div id="mn">navbar</div>
 | 
				
			||||||
 | 
						<div id="mh">
 | 
				
			||||||
 | 
							<a id="lightswitch" href="#">go dark</a>
 | 
				
			||||||
 | 
							<a id="navtoggle" href="#">hide nav</a>
 | 
				
			||||||
 | 
							{%- if edit %}
 | 
				
			||||||
 | 
								<a id="save" href="?edit">save</a>
 | 
				
			||||||
 | 
								<a id="sbs" href="#">sbs</a>
 | 
				
			||||||
 | 
								<a id="nsbs" href="#">editor</a>
 | 
				
			||||||
 | 
								<div id="toolsbox">
 | 
				
			||||||
 | 
									<a id="tools" href="#">tools</a>
 | 
				
			||||||
 | 
									<a id="fmt_table" href="#">prettify table (ctrl-k)</a>
 | 
				
			||||||
 | 
									<a id="iter_uni" href="#">non-ascii: iterate (ctrl-u)</a>
 | 
				
			||||||
 | 
									<a id="mark_uni" href="#">non-ascii: markup</a>
 | 
				
			||||||
 | 
									<a id="cfg_uni" href="#">non-ascii: whitelist</a>
 | 
				
			||||||
 | 
									<a id="help" href="#">help</a>
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
							{%- else %}
 | 
				
			||||||
 | 
								<a href="?edit">edit (basic)</a>
 | 
				
			||||||
 | 
								<a href="?edit2">edit (fancy)</a>
 | 
				
			||||||
 | 
								<a href="?raw">view raw</a>
 | 
				
			||||||
 | 
							{%- endif %}
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
	<div id="toc"></div>
 | 
						<div id="toc"></div>
 | 
				
			||||||
 | 
						<div id="mtw">
 | 
				
			||||||
 | 
							<textarea id="mt" autocomplete="off">{{ md }}</textarea>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
	<div id="mw">
 | 
						<div id="mw">
 | 
				
			||||||
		<div id="mh">
 | 
					 | 
				
			||||||
			<a id="lightswitch" href="#">go dark</a> //
 | 
					 | 
				
			||||||
			<a id="edit" href="?edit">edit this</a>
 | 
					 | 
				
			||||||
		</div>
 | 
					 | 
				
			||||||
		<div id="ml">
 | 
							<div id="ml">
 | 
				
			||||||
			<div style="text-align:center;margin:5em 0">
 | 
								<div style="text-align:center;margin:5em 0">
 | 
				
			||||||
				<div style="font-size:2em;margin:1em 0">Loading</div>
 | 
									<div style="font-size:2em;margin:1em 0">Loading</div>
 | 
				
			||||||
				if you're still reading this, check that javascript is allowed
 | 
									if you're still reading this, check that javascript is allowed
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
		</div>
 | 
							</div>
 | 
				
			||||||
		<div id="m">
 | 
							<div id="mp"></div>
 | 
				
			||||||
			<textarea id="mt" style="display:none">{{ md }}</textarea>
 | 
					 | 
				
			||||||
		</div>
 | 
					 | 
				
			||||||
	</div>
 | 
						</div>
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						{%- if edit %}
 | 
				
			||||||
 | 
						<div id="helpbox">
 | 
				
			||||||
 | 
							<textarea autocomplete="off">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					write markdown (most html is 🙆 too)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## hotkey list
 | 
				
			||||||
 | 
					* `Ctrl-S` to save
 | 
				
			||||||
 | 
					* `Ctrl-E` to toggle mode
 | 
				
			||||||
 | 
					* `Ctrl-K` to prettyprint a table
 | 
				
			||||||
 | 
					* `Ctrl-U` to iterate non-ascii chars
 | 
				
			||||||
 | 
					* `Ctrl-H` / `Ctrl-Shift-H` to create a header
 | 
				
			||||||
 | 
					* `TAB` / `Shift-TAB` to indent/dedent a selection
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## toolbar
 | 
				
			||||||
 | 
					1. toggle dark mode
 | 
				
			||||||
 | 
					2. show/hide navigation bar
 | 
				
			||||||
 | 
					3. save changes on server
 | 
				
			||||||
 | 
					4. side-by-side editing
 | 
				
			||||||
 | 
					5. toggle editor/preview
 | 
				
			||||||
 | 
					6. this thing :^)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## markdown
 | 
				
			||||||
 | 
					|||
 | 
				
			||||||
 | 
					|--|--|
 | 
				
			||||||
 | 
					|`**bold**`|**bold**|
 | 
				
			||||||
 | 
					|`_italic_`|_italic_|
 | 
				
			||||||
 | 
					|`~~strike~~`|~~strike~~|
 | 
				
			||||||
 | 
					|`` `code` ``|`code`|
 | 
				
			||||||
 | 
					|`[](#hotkey-list)`|[](#hotkey-list)|
 | 
				
			||||||
 | 
					|`[](/foo/bar.md#header)`|[](/foo/bar.md#header)|
 | 
				
			||||||
 | 
					|`<blink>💯</blink>`|<blink>💯</blink>|
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## tables
 | 
				
			||||||
 | 
					    |left-aligned|centered|right-aligned
 | 
				
			||||||
 | 
					    | ---------- | :----: | ----------:
 | 
				
			||||||
 | 
					    |one         |two     |three
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					|left-aligned|centered|right-aligned
 | 
				
			||||||
 | 
					| ---------- | :----: | ----------:
 | 
				
			||||||
 | 
					|one         |two     |three
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## lists
 | 
				
			||||||
 | 
						* one
 | 
				
			||||||
 | 
						* two
 | 
				
			||||||
 | 
						1. one
 | 
				
			||||||
 | 
						1. two
 | 
				
			||||||
 | 
					* one
 | 
				
			||||||
 | 
					* two
 | 
				
			||||||
 | 
					1. one
 | 
				
			||||||
 | 
					1. two
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## headers
 | 
				
			||||||
 | 
						# level 1
 | 
				
			||||||
 | 
						## level 2
 | 
				
			||||||
 | 
						### level 3
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## quote
 | 
				
			||||||
 | 
						> hello
 | 
				
			||||||
 | 
					> hello
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## codeblock
 | 
				
			||||||
 | 
							four spaces (no tab pls)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## code in lists
 | 
				
			||||||
 | 
						* foo
 | 
				
			||||||
 | 
						  bar
 | 
				
			||||||
 | 
					          six spaces total
 | 
				
			||||||
 | 
					* foo
 | 
				
			||||||
 | 
					  bar
 | 
				
			||||||
 | 
					      six spaces total
 | 
				
			||||||
 | 
					.
 | 
				
			||||||
 | 
							</textarea>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
						{%- endif %}
 | 
				
			||||||
 | 
						
 | 
				
			||||||
	<script>
 | 
						<script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var link_md_as_html = false;  // TODO (does nothing)
 | 
					var link_md_as_html = false;  // TODO (does nothing)
 | 
				
			||||||
 | 
					var last_modified = {{ lastmod }};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
(function () {
 | 
					(function () {
 | 
				
			||||||
    var btn = document.getElementById("lightswitch");
 | 
					    var btn = document.getElementById("lightswitch");
 | 
				
			||||||
    var toggle = function () {
 | 
					    var toggle = function (e) {
 | 
				
			||||||
 | 
							if (e) e.preventDefault();
 | 
				
			||||||
        var dark = !document.documentElement.getAttribute("class");
 | 
					        var dark = !document.documentElement.getAttribute("class");
 | 
				
			||||||
        document.documentElement.setAttribute("class", dark ? "dark" : "");
 | 
					        document.documentElement.setAttribute("class", dark ? "dark" : "");
 | 
				
			||||||
        btn.innerHTML = "go " + (dark ? "light" : "dark");
 | 
					        btn.innerHTML = "go " + (dark ? "light" : "dark");
 | 
				
			||||||
@@ -41,7 +141,17 @@ var link_md_as_html = false;  // TODO (does nothing)
 | 
				
			|||||||
		toggle();
 | 
							toggle();
 | 
				
			||||||
})();
 | 
					})();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if (!String.startsWith) {
 | 
				
			||||||
 | 
						String.prototype.startsWith = function(s, i) {
 | 
				
			||||||
 | 
							i = i>0 ? i|0 : 0;
 | 
				
			||||||
 | 
							return this.substring(i, i + s.length) === s;
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	</script>
 | 
						</script>
 | 
				
			||||||
	<script src="/.cpr/deps/marked.full.js"></script>
 | 
						<script src="/.cpr/deps/marked.full.js"></script>
 | 
				
			||||||
	<script src="/.cpr/md.js"></script>
 | 
						<script src="/.cpr/md.js"></script>
 | 
				
			||||||
 | 
						{%- if edit %}
 | 
				
			||||||
 | 
							<script src="/.cpr/md2.js"></script>
 | 
				
			||||||
 | 
						{%- endif %}
 | 
				
			||||||
</body></html>
 | 
					</body></html>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,17 +1,54 @@
 | 
				
			|||||||
/*var conv = new showdown.Converter();
 | 
					 | 
				
			||||||
conv.setFlavor('github');
 | 
					 | 
				
			||||||
conv.setOption('tasklists', 0);
 | 
					 | 
				
			||||||
var mhtml = conv.makeHtml(dom_md.value);
 | 
					 | 
				
			||||||
*/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
var dom_toc = document.getElementById('toc');
 | 
					var dom_toc = document.getElementById('toc');
 | 
				
			||||||
var dom_wrap = document.getElementById('mw');
 | 
					var dom_wrap = document.getElementById('mw');
 | 
				
			||||||
var dom_head = document.getElementById('mh');
 | 
					var dom_hbar = document.getElementById('mh');
 | 
				
			||||||
var dom_nav = document.getElementById('mn');
 | 
					var dom_nav = document.getElementById('mn');
 | 
				
			||||||
var dom_doc = document.getElementById('m');
 | 
					var dom_pre = document.getElementById('mp');
 | 
				
			||||||
var dom_md = document.getElementById('mt');
 | 
					var dom_src = document.getElementById('mt');
 | 
				
			||||||
 | 
					var dom_navtgl = document.getElementById('navtoggle');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// add toolbar buttons
 | 
					
 | 
				
			||||||
 | 
					// chrome 49 needs this
 | 
				
			||||||
 | 
					var chromedbg = function () { console.log(arguments); }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// null-logger
 | 
				
			||||||
 | 
					var dbg = function () { };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// replace dbg with the real deal here or in the console:
 | 
				
			||||||
 | 
					// dbg = chromedbg
 | 
				
			||||||
 | 
					// dbg = console.log
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function hesc(txt) {
 | 
				
			||||||
 | 
					    return txt.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function cls(dom, name, add) {
 | 
				
			||||||
 | 
					    var re = new RegExp('(^| )' + name + '( |$)');
 | 
				
			||||||
 | 
					    var lst = (dom.getAttribute('class') + '').replace(re, "$1$2").replace(/  /, "");
 | 
				
			||||||
 | 
					    dom.setAttribute('class', lst + (add ? ' ' + name : ''));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function static(obj) {
 | 
				
			||||||
 | 
					    return JSON.parse(JSON.stringify(obj));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// dodge browser issues
 | 
				
			||||||
 | 
					(function () {
 | 
				
			||||||
 | 
					    var ua = navigator.userAgent;
 | 
				
			||||||
 | 
					    if (ua.indexOf(') Gecko/') !== -1 && /Linux| Mac /.exec(ua)) {
 | 
				
			||||||
 | 
					        // necessary on ff-68.7 at least
 | 
				
			||||||
 | 
					        var s = document.createElement('style');
 | 
				
			||||||
 | 
					        s.innerHTML = '@page { margin: .5in .6in .8in .6in; }';
 | 
				
			||||||
 | 
					        console.log(s.innerHTML);
 | 
				
			||||||
 | 
					        document.head.appendChild(s);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					})();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// add navbar
 | 
				
			||||||
(function () {
 | 
					(function () {
 | 
				
			||||||
    var n = document.location + '';
 | 
					    var n = document.location + '';
 | 
				
			||||||
    n = n.substr(n.indexOf('//') + 2).split('?')[0].split('/');
 | 
					    n = n.substr(n.indexOf('//') + 2).split('?')[0].split('/');
 | 
				
			||||||
@@ -22,25 +59,123 @@ var dom_md = document.getElementById('mt');
 | 
				
			|||||||
        if (a > 0)
 | 
					        if (a > 0)
 | 
				
			||||||
            loc.push(n[a]);
 | 
					            loc.push(n[a]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        nav.push('<a href="/' + loc.join('/') + '">' + n[a] + '</a>');
 | 
					        var dec = hesc(decodeURIComponent(n[a]));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        nav.push('<a href="/' + loc.join('/') + '">' + dec + '</a>');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    dom_nav.innerHTML = nav.join('');
 | 
					    dom_nav.innerHTML = nav.join('');
 | 
				
			||||||
})();
 | 
					})();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function convert_markdown(md_text) {
 | 
					
 | 
				
			||||||
 | 
					// faster than replacing the entire html (chrome 1.8x, firefox 1.6x)
 | 
				
			||||||
 | 
					function copydom(src, dst, lv) {
 | 
				
			||||||
 | 
					    var sc = src.childNodes,
 | 
				
			||||||
 | 
					        dc = dst.childNodes;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (sc.length !== dc.length) {
 | 
				
			||||||
 | 
					        dbg("replace L%d (%d/%d) |%d|",
 | 
				
			||||||
 | 
					            lv, sc.length, dc.length, src.innerHTML.length);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        dst.innerHTML = src.innerHTML;
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var rpl = [];
 | 
				
			||||||
 | 
					    for (var a = sc.length - 1; a >= 0; a--) {
 | 
				
			||||||
 | 
					        var st = sc[a].tagName,
 | 
				
			||||||
 | 
					            dt = dc[a].tagName;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (st !== dt) {
 | 
				
			||||||
 | 
					            dbg("replace L%d (%d/%d) type %s/%s", lv, a, sc.length, st, dt);
 | 
				
			||||||
 | 
					            rpl.push(a);
 | 
				
			||||||
 | 
					            continue;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var sa = sc[a].attributes || [],
 | 
				
			||||||
 | 
					            da = dc[a].attributes || [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (sa.length !== da.length) {
 | 
				
			||||||
 | 
					            dbg("replace L%d (%d/%d) attr# %d/%d",
 | 
				
			||||||
 | 
					                lv, a, sc.length, sa.length, da.length);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            rpl.push(a);
 | 
				
			||||||
 | 
					            continue;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var dirty = false;
 | 
				
			||||||
 | 
					        for (var b = sa.length - 1; b >= 0; b--) {
 | 
				
			||||||
 | 
					            var name = sa[b].name,
 | 
				
			||||||
 | 
					                sv = sa[b].value,
 | 
				
			||||||
 | 
					                dv = dc[a].getAttribute(name);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (name == "data-ln" && sv !== dv) {
 | 
				
			||||||
 | 
					                dc[a].setAttribute(name, sv);
 | 
				
			||||||
 | 
					                continue;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (sv !== dv) {
 | 
				
			||||||
 | 
					                dbg("replace L%d (%d/%d) attr %s [%s] [%s]",
 | 
				
			||||||
 | 
					                    lv, a, sc.length, name, sv, dv);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                dirty = true;
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (dirty)
 | 
				
			||||||
 | 
					            rpl.push(a);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // TODO pure guessing
 | 
				
			||||||
 | 
					    if (rpl.length > sc.length / 3) {
 | 
				
			||||||
 | 
					        dbg("replace L%d fully, %s (%d/%d) |%d|",
 | 
				
			||||||
 | 
					            lv, rpl.length, sc.length, src.innerHTML.length);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        dst.innerHTML = src.innerHTML;
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // repl is reversed; build top-down
 | 
				
			||||||
 | 
					    var nbytes = 0;
 | 
				
			||||||
 | 
					    for (var a = rpl.length - 1; a >= 0; a--) {
 | 
				
			||||||
 | 
					        var html = sc[rpl[a]].outerHTML;
 | 
				
			||||||
 | 
					        dc[rpl[a]].outerHTML = html;
 | 
				
			||||||
 | 
					        nbytes += html.length;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (nbytes > 0)
 | 
				
			||||||
 | 
					        dbg("replaced %d bytes L%d", nbytes, lv);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (var a = 0; a < sc.length; a++)
 | 
				
			||||||
 | 
					        copydom(sc[a], dc[a], lv + 1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (src.innerHTML !== dst.innerHTML) {
 | 
				
			||||||
 | 
					        dbg("setting %d bytes L%d", src.innerHTML.length, lv);
 | 
				
			||||||
 | 
					        dst.innerHTML = src.innerHTML;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function convert_markdown(md_text, dest_dom) {
 | 
				
			||||||
    marked.setOptions({
 | 
					    marked.setOptions({
 | 
				
			||||||
        //headerPrefix: 'h-',
 | 
					        //headerPrefix: 'h-',
 | 
				
			||||||
        breaks: true,
 | 
					        breaks: true,
 | 
				
			||||||
        gfm: true
 | 
					        gfm: true
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    var html = marked(md_text);
 | 
					    var md_html = marked(md_text);
 | 
				
			||||||
    dom_doc.innerHTML = html;
 | 
					    var md_dom = new DOMParser().parseFromString(md_html, "text/html").body;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var loader = document.getElementById('ml');
 | 
					    var nodes = md_dom.getElementsByTagName('a');
 | 
				
			||||||
    loader.parentNode.removeChild(loader);
 | 
					    for (var a = nodes.length - 1; a >= 0; a--) {
 | 
				
			||||||
 | 
					        var href = nodes[a].getAttribute('href');
 | 
				
			||||||
 | 
					        var txt = nodes[a].textContent;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (!txt)
 | 
				
			||||||
 | 
					            nodes[a].textContent = href;
 | 
				
			||||||
 | 
					        else if (href !== txt)
 | 
				
			||||||
 | 
					            nodes[a].setAttribute('class', 'vis');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // todo-lists (should probably be a marked extension)
 | 
					    // todo-lists (should probably be a marked extension)
 | 
				
			||||||
    var nodes = dom_doc.getElementsByTagName('input');
 | 
					    nodes = md_dom.getElementsByTagName('input');
 | 
				
			||||||
    for (var a = nodes.length - 1; a >= 0; a--) {
 | 
					    for (var a = nodes.length - 1; a >= 0; a--) {
 | 
				
			||||||
        var dom_box = nodes[a];
 | 
					        var dom_box = nodes[a];
 | 
				
			||||||
        if (dom_box.getAttribute('type') !== 'checkbox')
 | 
					        if (dom_box.getAttribute('type') !== 'checkbox')
 | 
				
			||||||
@@ -59,34 +194,77 @@ function convert_markdown(md_text) {
 | 
				
			|||||||
            '<span class="todo_' + clas + '">' + char + '</span>' +
 | 
					            '<span class="todo_' + clas + '">' + char + '</span>' +
 | 
				
			||||||
            html.substr(html.indexOf('>') + 1);
 | 
					            html.substr(html.indexOf('>') + 1);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // separate <code> for each line in <pre>
 | 
				
			||||||
 | 
					    var nodes = md_dom.getElementsByTagName('pre');
 | 
				
			||||||
 | 
					    for (var a = nodes.length - 1; a >= 0; a--) {
 | 
				
			||||||
 | 
					        var el = nodes[a];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var is_precode =
 | 
				
			||||||
 | 
					            el.tagName == 'PRE' &&
 | 
				
			||||||
 | 
					            el.childNodes.length === 1 &&
 | 
				
			||||||
 | 
					            el.childNodes[0].tagName == 'CODE';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (!is_precode)
 | 
				
			||||||
 | 
					            continue;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var nline = parseInt(el.getAttribute('data-ln')) + 1;
 | 
				
			||||||
 | 
					        var lines = el.innerHTML.replace(/\r?\n<\/code>$/i, '</code>').split(/\r?\n/g);
 | 
				
			||||||
 | 
					        for (var b = 0; b < lines.length - 1; b++)
 | 
				
			||||||
 | 
					            lines[b] += '</code>\n<code data-ln="' + (nline + b) + '">';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        el.innerHTML = lines.join('');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // self-link headers
 | 
				
			||||||
 | 
					    var id_seen = {},
 | 
				
			||||||
 | 
					        dyn = md_dom.getElementsByTagName('*');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    nodes = [];
 | 
				
			||||||
 | 
					    for (var a = 0, aa = dyn.length; a < aa; a++)
 | 
				
			||||||
 | 
					        if (/^[Hh]([1-6])/.exec(dyn[a].tagName) !== null)
 | 
				
			||||||
 | 
					            nodes.push(dyn[a]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (var a = 0; a < nodes.length; a++) {
 | 
				
			||||||
 | 
					        el = nodes[a];
 | 
				
			||||||
 | 
					        var id = el.getAttribute('id'),
 | 
				
			||||||
 | 
					            orig_id = id;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (id_seen[id]) {
 | 
				
			||||||
 | 
					            for (var n = 1; n < 4096; n++) {
 | 
				
			||||||
 | 
					                id = orig_id + '-' + n;
 | 
				
			||||||
 | 
					                if (!id_seen[id])
 | 
				
			||||||
 | 
					                    break;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            el.setAttribute('id', id);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        id_seen[id] = 1;
 | 
				
			||||||
 | 
					        el.innerHTML = '<a href="#' + id + '">' + el.innerHTML + '</a>';
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    copydom(md_dom, dest_dom, 0);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function init_toc() {
 | 
					function init_toc() {
 | 
				
			||||||
 | 
					    var loader = document.getElementById('ml');
 | 
				
			||||||
 | 
					    loader.parentNode.removeChild(loader);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var anchors = [];  // list of toc entries, complex objects
 | 
					    var anchors = [];  // list of toc entries, complex objects
 | 
				
			||||||
    var anchor = null; // current toc node
 | 
					    var anchor = null; // current toc node
 | 
				
			||||||
    var id_seen = {};  // taken IDs
 | 
					 | 
				
			||||||
    var html = [];     // generated toc html
 | 
					    var html = [];     // generated toc html
 | 
				
			||||||
    var lv = 0;        // current indentation level in the toc html
 | 
					    var lv = 0;        // current indentation level in the toc html
 | 
				
			||||||
    var re = new RegExp('^[Hh]([1-3])');
 | 
					    var ctr = [0, 0, 0, 0, 0, 0];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var manip_nodes_dyn = dom_doc.getElementsByTagName('*');
 | 
					    var manip_nodes_dyn = dom_pre.getElementsByTagName('*');
 | 
				
			||||||
    var manip_nodes = [];
 | 
					    var manip_nodes = [];
 | 
				
			||||||
    for (var a = 0, aa = manip_nodes_dyn.length; a < aa; a++)
 | 
					    for (var a = 0, aa = manip_nodes_dyn.length; a < aa; a++)
 | 
				
			||||||
        manip_nodes.push(manip_nodes_dyn[a]);
 | 
					        manip_nodes.push(manip_nodes_dyn[a]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for (var a = 0, aa = manip_nodes.length; a < aa; a++) {
 | 
					    for (var a = 0, aa = manip_nodes.length; a < aa; a++) {
 | 
				
			||||||
        var elm = manip_nodes[a];
 | 
					        var elm = manip_nodes[a];
 | 
				
			||||||
        var m = re.exec(elm.tagName);
 | 
					        var m = /^[Hh]([1-6])/.exec(elm.tagName);
 | 
				
			||||||
 | 
					        var is_header = m !== null;
 | 
				
			||||||
        var is_header =
 | 
					 | 
				
			||||||
            m !== null;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        var is_precode =
 | 
					 | 
				
			||||||
            !is_header &&
 | 
					 | 
				
			||||||
            elm.tagName == 'PRE' &&
 | 
					 | 
				
			||||||
            elm.childNodes.length === 1 &&
 | 
					 | 
				
			||||||
            elm.childNodes[0].tagName == 'CODE';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (is_header) {
 | 
					        if (is_header) {
 | 
				
			||||||
            var nlv = m[1];
 | 
					            var nlv = m[1];
 | 
				
			||||||
            while (lv < nlv) {
 | 
					            while (lv < nlv) {
 | 
				
			||||||
@@ -97,24 +275,13 @@ function init_toc() {
 | 
				
			|||||||
                html.push('</ul>');
 | 
					                html.push('</ul>');
 | 
				
			||||||
                lv--;
 | 
					                lv--;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					            ctr[lv - 1]++;
 | 
				
			||||||
 | 
					            for (var b = lv; b < 6; b++)
 | 
				
			||||||
 | 
					                ctr[b] = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            var orig_id = elm.getAttribute('id');
 | 
					            elm.childNodes[0].setAttribute('ctr', ctr.slice(0, lv).join('.'));
 | 
				
			||||||
            var id = orig_id;
 | 
					 | 
				
			||||||
            if (id_seen[id]) {
 | 
					 | 
				
			||||||
                for (var n = 1; n < 4096; n++) {
 | 
					 | 
				
			||||||
                    id = orig_id + '-' + n;
 | 
					 | 
				
			||||||
                    if (!id_seen[id])
 | 
					 | 
				
			||||||
                        break;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                elm.setAttribute('id', id);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            id_seen[id] = 1;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            var ahref = '<a href="#' + id + '">' +
 | 
					            html.push('<li>' + elm.innerHTML + '</li>');
 | 
				
			||||||
                elm.innerHTML + '</a>';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            html.push('<li>' + ahref + '</li>');
 | 
					 | 
				
			||||||
            elm.innerHTML = ahref;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (anchor != null)
 | 
					            if (anchor != null)
 | 
				
			||||||
                anchors.push(anchor);
 | 
					                anchors.push(anchor);
 | 
				
			||||||
@@ -125,17 +292,6 @@ function init_toc() {
 | 
				
			|||||||
                y: null
 | 
					                y: null
 | 
				
			||||||
            };
 | 
					            };
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        else if (is_precode) {
 | 
					 | 
				
			||||||
            // not actually toc-related (sorry),
 | 
					 | 
				
			||||||
            // split <pre><code /></pre> into one <code> per line
 | 
					 | 
				
			||||||
            var nline = parseInt(elm.getAttribute('data-ln')) + 1;
 | 
					 | 
				
			||||||
            var lines = elm.innerHTML.replace(/\r?\n<\/code>$/i, '</code>').split(/\r?\n/g);
 | 
					 | 
				
			||||||
            for (var b = 0; b < lines.length - 1; b++)
 | 
					 | 
				
			||||||
                lines[b] += '</code>\n<code data-ln="' + (nline + b) + '">';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            elm.innerHTML = lines.join('');
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (!is_header && anchor)
 | 
					        if (!is_header && anchor)
 | 
				
			||||||
            anchor.kids.push(elm);
 | 
					            anchor.kids.push(elm);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -207,41 +363,47 @@ function init_toc() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// "main" :p
 | 
					// "main" :p
 | 
				
			||||||
convert_markdown(dom_md.value);
 | 
					convert_markdown(dom_src.value, dom_pre);
 | 
				
			||||||
var toc = init_toc();
 | 
					var toc = init_toc();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// scroll handler
 | 
					// scroll handler
 | 
				
			||||||
(function () {
 | 
					var redraw = (function () {
 | 
				
			||||||
    var timer_active = false;
 | 
					    var sbs = false;
 | 
				
			||||||
    var final = null;
 | 
					    function onresize() {
 | 
				
			||||||
 | 
					        sbs = window.matchMedia('(min-width: 64em)').matches;
 | 
				
			||||||
 | 
					        var y = (dom_hbar.offsetTop + dom_hbar.offsetHeight) + 'px';
 | 
				
			||||||
 | 
					        if (sbs) {
 | 
				
			||||||
 | 
					            dom_toc.style.top = y;
 | 
				
			||||||
 | 
					            dom_wrap.style.top = y;
 | 
				
			||||||
 | 
					            dom_toc.style.marginTop = '0';
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        onscroll();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function onscroll() {
 | 
					    function onscroll() {
 | 
				
			||||||
        clearTimeout(final);
 | 
					 | 
				
			||||||
        timer_active = false;
 | 
					 | 
				
			||||||
        toc.refresh();
 | 
					        toc.refresh();
 | 
				
			||||||
 | 
					 | 
				
			||||||
        var y = 0;
 | 
					 | 
				
			||||||
        if (window.matchMedia('(min-width: 64em)').matches)
 | 
					 | 
				
			||||||
            y = parseInt(dom_nav.offsetHeight) - window.scrollY;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        dom_toc.style.marginTop = y < 0 ? 0 : y + "px";
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    onscroll();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function ev_onscroll() {
 | 
					    window.onresize = onresize;
 | 
				
			||||||
        // long timeout: scroll ended
 | 
					    window.onscroll = onscroll;
 | 
				
			||||||
        clearTimeout(final);
 | 
					    dom_wrap.onscroll = onscroll;
 | 
				
			||||||
        final = setTimeout(onscroll, 100);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // short timeout: continuous updates
 | 
					    onresize();
 | 
				
			||||||
        if (timer_active)
 | 
					    return onresize;
 | 
				
			||||||
            return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        timer_active = true;
 | 
					 | 
				
			||||||
        setTimeout(onscroll, 10);
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    window.onscroll = ev_onscroll;
 | 
					 | 
				
			||||||
    window.onresize = ev_onscroll;
 | 
					 | 
				
			||||||
})();
 | 
					})();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					dom_navtgl.onclick = function () {
 | 
				
			||||||
 | 
					    var hidden = dom_navtgl.innerHTML == 'hide nav';
 | 
				
			||||||
 | 
					    dom_navtgl.innerHTML = hidden ? 'show nav' : 'hide nav';
 | 
				
			||||||
 | 
					    dom_nav.style.display = hidden ? 'none' : 'block';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (window.localStorage)
 | 
				
			||||||
 | 
					        localStorage.setItem('hidenav', hidden ? 1 : 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    redraw();
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if (window.localStorage && localStorage.getItem('hidenav') == 1)
 | 
				
			||||||
 | 
					    dom_navtgl.onclick();
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										108
									
								
								copyparty/web/md2.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								copyparty/web/md2.css
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,108 @@
 | 
				
			|||||||
 | 
					#toc {
 | 
				
			||||||
 | 
					    display: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					#mtw {
 | 
				
			||||||
 | 
					    display: block;
 | 
				
			||||||
 | 
					    position: fixed;
 | 
				
			||||||
 | 
					    left: .5em;
 | 
				
			||||||
 | 
					    bottom: 0;
 | 
				
			||||||
 | 
					    width: calc(100% - 56em);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					#mw {
 | 
				
			||||||
 | 
					    left: calc(100% - 55em);
 | 
				
			||||||
 | 
					    overflow-y: auto;
 | 
				
			||||||
 | 
					    position: fixed;
 | 
				
			||||||
 | 
					    bottom: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* single-screen */
 | 
				
			||||||
 | 
					#mtw.preview,
 | 
				
			||||||
 | 
					#mw.editor {
 | 
				
			||||||
 | 
					    opacity: 0;
 | 
				
			||||||
 | 
					    z-index: 1;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					#mw.preview,
 | 
				
			||||||
 | 
					#mtw.editor {
 | 
				
			||||||
 | 
					    z-index: 5;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					#mtw.single,
 | 
				
			||||||
 | 
					#mw.single {
 | 
				
			||||||
 | 
					    margin: 0;
 | 
				
			||||||
 | 
					    left: 1em;
 | 
				
			||||||
 | 
					    left: max(1em, calc((100% - 56em) / 2));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					#mtw.single {
 | 
				
			||||||
 | 
					    width: 55em;
 | 
				
			||||||
 | 
					    width: min(55em, calc(100% - 2em));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#mp {
 | 
				
			||||||
 | 
					    position: relative;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					#mt, #mtr {
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					    height: calc(100% - 1px);
 | 
				
			||||||
 | 
					    color: #444;
 | 
				
			||||||
 | 
					    background: #f7f7f7;
 | 
				
			||||||
 | 
					    border: 1px solid #999;
 | 
				
			||||||
 | 
					    outline: none;
 | 
				
			||||||
 | 
					    padding: 0;
 | 
				
			||||||
 | 
					    margin: 0;
 | 
				
			||||||
 | 
					    font-family: 'consolas', monospace, monospace;
 | 
				
			||||||
 | 
					    white-space: pre-wrap;
 | 
				
			||||||
 | 
					    word-break: break-word;
 | 
				
			||||||
 | 
					    overflow-wrap: break-word;
 | 
				
			||||||
 | 
					    word-wrap: break-word; /*ie*/
 | 
				
			||||||
 | 
					    overflow-y: scroll;
 | 
				
			||||||
 | 
					    line-height: 1.3em;
 | 
				
			||||||
 | 
					    font-size: .9em;
 | 
				
			||||||
 | 
					    position: relative;
 | 
				
			||||||
 | 
					    scrollbar-color: #eb0 #f7f7f7;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					html.dark #mt {
 | 
				
			||||||
 | 
					    color: #eee;
 | 
				
			||||||
 | 
					    background: #222;
 | 
				
			||||||
 | 
					    border: 1px solid #777;
 | 
				
			||||||
 | 
					    scrollbar-color: #b80 #282828;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					#mtr {
 | 
				
			||||||
 | 
					    position: absolute;
 | 
				
			||||||
 | 
					    top: 0;
 | 
				
			||||||
 | 
					    left: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					#save.force-save {
 | 
				
			||||||
 | 
					    color: #400;
 | 
				
			||||||
 | 
					    background: #f97;
 | 
				
			||||||
 | 
					    border-radius: .15em;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					#save.disabled {
 | 
				
			||||||
 | 
					    opacity: .4;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					#helpbox {
 | 
				
			||||||
 | 
					    display: none;
 | 
				
			||||||
 | 
					    position: fixed;
 | 
				
			||||||
 | 
					    background: #f7f7f7;
 | 
				
			||||||
 | 
					    box-shadow: 0 .5em 2em #777;
 | 
				
			||||||
 | 
					    border-radius: .4em;
 | 
				
			||||||
 | 
					    padding: 2em;
 | 
				
			||||||
 | 
					    top: 4em;
 | 
				
			||||||
 | 
					    overflow-y: auto;
 | 
				
			||||||
 | 
					    height: calc(100% - 12em);
 | 
				
			||||||
 | 
					    left: calc(50% - 15em);
 | 
				
			||||||
 | 
					    right: 0;
 | 
				
			||||||
 | 
					    width: 30em;
 | 
				
			||||||
 | 
					    z-index: 9001;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					#helpclose {
 | 
				
			||||||
 | 
					    display: block;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					html.dark #helpbox {
 | 
				
			||||||
 | 
					    background: #222;
 | 
				
			||||||
 | 
					    box-shadow: 0 .5em 2em #444;
 | 
				
			||||||
 | 
					    border: 1px solid #079;
 | 
				
			||||||
 | 
					    border-width: 1px 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# mt {opacity: .5;top:1px}
 | 
				
			||||||
							
								
								
									
										1010
									
								
								copyparty/web/md2.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1010
									
								
								copyparty/web/md2.js
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -21,7 +21,6 @@ html, body {
 | 
				
			|||||||
#mn {
 | 
					#mn {
 | 
				
			||||||
    font-weight: normal;
 | 
					    font-weight: normal;
 | 
				
			||||||
    margin: 1.3em 0 .7em 1em;
 | 
					    margin: 1.3em 0 .7em 1em;
 | 
				
			||||||
    font-size: 1.4em;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
#mn a {
 | 
					#mn a {
 | 
				
			||||||
    color: #444;
 | 
					    color: #444;
 | 
				
			||||||
@@ -44,8 +43,8 @@ html, body {
 | 
				
			|||||||
    height: 1.05em;
 | 
					    height: 1.05em;
 | 
				
			||||||
    margin: -.2em .3em -.2em -.4em;
 | 
					    margin: -.2em .3em -.2em -.4em;
 | 
				
			||||||
    display: inline-block;
 | 
					    display: inline-block;
 | 
				
			||||||
    border: 1px solid rgba(0,0,0,0.3);
 | 
					    border: 1px solid rgba(0,0,0,0.2);
 | 
				
			||||||
    border-width: .05em .05em 0 0;
 | 
					    border-width: .2em .2em 0 0;
 | 
				
			||||||
    transform: rotate(45deg);
 | 
					    transform: rotate(45deg);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
#mn a:hover {
 | 
					#mn a:hover {
 | 
				
			||||||
@@ -161,8 +160,12 @@ h2 {
 | 
				
			|||||||
.mdo ol>li {
 | 
					.mdo ol>li {
 | 
				
			||||||
	margin: .7em 0;
 | 
						margin: .7em 0;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					strong {
 | 
				
			||||||
 | 
						color: #000;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
p>em,
 | 
					p>em,
 | 
				
			||||||
li>em {
 | 
					li>em,
 | 
				
			||||||
 | 
					td>em {
 | 
				
			||||||
	color: #c50;
 | 
						color: #c50;
 | 
				
			||||||
	padding: .1em;
 | 
						padding: .1em;
 | 
				
			||||||
	border-bottom: .1em solid #bbb;
 | 
						border-bottom: .1em solid #bbb;
 | 
				
			||||||
@@ -254,8 +257,12 @@ html.dark .mdo>ul,
 | 
				
			|||||||
html.dark .mdo>ol {
 | 
					html.dark .mdo>ol {
 | 
				
			||||||
    border-color: #555;
 | 
					    border-color: #555;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					html.dark strong {
 | 
				
			||||||
 | 
					    color: #fff;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
html.dark p>em,
 | 
					html.dark p>em,
 | 
				
			||||||
html.dark li>em {
 | 
					html.dark li>em,
 | 
				
			||||||
 | 
					html.dark td>em {
 | 
				
			||||||
    color: #f94;
 | 
					    color: #f94;
 | 
				
			||||||
    border-color: #666;
 | 
					    border-color: #666;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,7 +17,7 @@
 | 
				
			|||||||
			</div>
 | 
								</div>
 | 
				
			||||||
		</div>
 | 
							</div>
 | 
				
			||||||
		<div id="m">
 | 
							<div id="m">
 | 
				
			||||||
			<textarea id="mt" style="display:none">{{ md }}</textarea>
 | 
								<textarea id="mt" style="display:none" autocomplete="off">{{ md }}</textarea>
 | 
				
			||||||
		</div>
 | 
							</div>
 | 
				
			||||||
	</div>
 | 
						</div>
 | 
				
			||||||
	<script>
 | 
						<script>
 | 
				
			||||||
@@ -39,6 +39,6 @@ var lightswitch = (function () {
 | 
				
			|||||||
})();
 | 
					})();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	</script>
 | 
						</script>
 | 
				
			||||||
	<script src="/.cpr/deps/easymde.full.js"></script>
 | 
						<script src="/.cpr/deps/easymde.js"></script>
 | 
				
			||||||
	<script src="/.cpr/mde.js"></script>
 | 
						<script src="/.cpr/mde.js"></script>
 | 
				
			||||||
</body></html>
 | 
					</body></html>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -13,7 +13,9 @@ var dom_md = document.getElementById('mt');
 | 
				
			|||||||
        if (a > 0)
 | 
					        if (a > 0)
 | 
				
			||||||
            loc.push(n[a]);
 | 
					            loc.push(n[a]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        nav.push('<a href="/' + loc.join('/') + '">' + n[a] + '</a>');
 | 
					        var dec = decodeURIComponent(n[a]).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        nav.push('<a href="/' + loc.join('/') + '">' + dec + '</a>');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    dom_nav.innerHTML = nav.join('');
 | 
					    dom_nav.innerHTML = nav.join('');
 | 
				
			||||||
})();
 | 
					})();
 | 
				
			||||||
@@ -51,7 +53,8 @@ var mde = (function () {
 | 
				
			|||||||
            "save": "Ctrl-S"
 | 
					            "save": "Ctrl-S"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        insertTexts: ["[](", ")"],
 | 
					        insertTexts: ["[](", ")"],
 | 
				
			||||||
        tabSize: 4,
 | 
					        indentWithTabs: false,
 | 
				
			||||||
 | 
					        tabSize: 2,
 | 
				
			||||||
        toolbar: tbar,
 | 
					        toolbar: tbar,
 | 
				
			||||||
        previewClass: 'mdo',
 | 
					        previewClass: 'mdo',
 | 
				
			||||||
        onToggleFullScreen: set_jumpto,
 | 
					        onToggleFullScreen: set_jumpto,
 | 
				
			||||||
@@ -118,7 +121,7 @@ function save(mde) {
 | 
				
			|||||||
    fd.append("lastmod", (force ? -1 : last_modified));
 | 
					    fd.append("lastmod", (force ? -1 : last_modified));
 | 
				
			||||||
    fd.append("body", txt);
 | 
					    fd.append("body", txt);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var url = (document.location + '').split('?')[0] + '?raw';
 | 
					    var url = (document.location + '').split('?')[0];
 | 
				
			||||||
    var xhr = new XMLHttpRequest();
 | 
					    var xhr = new XMLHttpRequest();
 | 
				
			||||||
    xhr.open('POST', url, true);
 | 
					    xhr.open('POST', url, true);
 | 
				
			||||||
    xhr.responseType = 'text';
 | 
					    xhr.responseType = 'text';
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -13,6 +13,7 @@ h1 {
 | 
				
			|||||||
	border-bottom: 1px solid #ccc;
 | 
						border-bottom: 1px solid #ccc;
 | 
				
			||||||
	margin: 2em 0 .4em 0;
 | 
						margin: 2em 0 .4em 0;
 | 
				
			||||||
	padding: 0 0 .2em 0;
 | 
						padding: 0 0 .2em 0;
 | 
				
			||||||
 | 
						font-weight: normal;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
li {
 | 
					li {
 | 
				
			||||||
	margin: 1em 0;
 | 
						margin: 1em 0;
 | 
				
			||||||
@@ -25,3 +26,28 @@ a {
 | 
				
			|||||||
	border-radius: .2em;
 | 
						border-radius: .2em;
 | 
				
			||||||
	padding: .2em .8em;
 | 
						padding: .2em .8em;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					html.dark,
 | 
				
			||||||
 | 
					html.dark body,
 | 
				
			||||||
 | 
					html.dark #wrap {
 | 
				
			||||||
 | 
						background: #222;
 | 
				
			||||||
 | 
						color: #ccc;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					html.dark h1 {
 | 
				
			||||||
 | 
						border-color: #777;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					html.dark a {
 | 
				
			||||||
 | 
						color: #fff;
 | 
				
			||||||
 | 
						background: #057;
 | 
				
			||||||
 | 
						border-color: #37a;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					html.dark input {
 | 
				
			||||||
 | 
						color: #fff;
 | 
				
			||||||
 | 
						background: #624;
 | 
				
			||||||
 | 
						border: 1px solid #c27;
 | 
				
			||||||
 | 
						border-width: 1px 0 0 0;
 | 
				
			||||||
 | 
						border-radius: .5em;
 | 
				
			||||||
 | 
						padding: .5em .7em;
 | 
				
			||||||
 | 
						margin: 0 .5em 0 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -36,7 +36,11 @@
 | 
				
			|||||||
            </form>
 | 
					            </form>
 | 
				
			||||||
        </ul>
 | 
					        </ul>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <!-- script src="/.cpr/splash.js"></script -->
 | 
					    <script>
 | 
				
			||||||
</body>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if (window.localStorage && localStorage.getItem('darkmode') == 1)
 | 
				
			||||||
 | 
					    document.documentElement.setAttribute("class", "dark");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					</body>
 | 
				
			||||||
</html>
 | 
					</html>
 | 
				
			||||||
@@ -34,6 +34,7 @@ window.onerror = function (msg, url, lineNo, columnNo, error) {
 | 
				
			|||||||
                    esc(String(error[find[a]])).replace(/\n/g, '<br />\n'));
 | 
					                    esc(String(error[find[a]])).replace(/\n/g, '<br />\n'));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    document.body.style.fontSize = '0.8em';
 | 
					    document.body.style.fontSize = '0.8em';
 | 
				
			||||||
 | 
					    document.body.style.padding = '0 1em 1em 1em';
 | 
				
			||||||
    hcroak(html.join('\n'));
 | 
					    hcroak(html.join('\n'));
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -211,7 +212,8 @@ function up2k_init(have_crypto) {
 | 
				
			|||||||
    // handle user intent to use the basic uploader instead
 | 
					    // handle user intent to use the basic uploader instead
 | 
				
			||||||
    o('u2nope').onclick = function (e) {
 | 
					    o('u2nope').onclick = function (e) {
 | 
				
			||||||
        e.preventDefault();
 | 
					        e.preventDefault();
 | 
				
			||||||
        un2k();
 | 
					        setmsg('');
 | 
				
			||||||
 | 
					        goto('bup');
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!String.prototype.format) {
 | 
					    if (!String.prototype.format) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -66,5 +66,5 @@
 | 
				
			|||||||
            </table>
 | 
					            </table>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <p id="u2foot"></p>
 | 
					            <p id="u2foot"></p>
 | 
				
			||||||
            <p>( if you don't need lastmod timestamps, resumable uploads or progress bars just use the <a href="#" id="u2nope" onclick="javascript:goto('bup');">basic uploader</a>)</p>
 | 
					            <p>( if you don't need lastmod timestamps, resumable uploads or progress bars just use the <a href="#" id="u2nope">basic uploader</a>)</p>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -13,7 +13,7 @@ head -c $((2*1024*1024*1024)) /dev/zero | openssl enc -aes-256-ctr -pass pass:hu
 | 
				
			|||||||
## testing multiple parallel uploads
 | 
					## testing multiple parallel uploads
 | 
				
			||||||
## usage:  para | tee log
 | 
					## usage:  para | tee log
 | 
				
			||||||
 | 
					
 | 
				
			||||||
para() { for s in 1 2 3 4 5 6 7 8 12 16 24 32 48 64; do echo $s; for r in {1..4}; do for ((n=0;n<s;n++)); do curl -sF "act=bput" -F "f=@garbage.file" http://127.0.0.1:1234/ 2>&1 & done; wait; echo; done; done; }
 | 
					para() { for s in 1 2 3 4 5 6 7 8 12 16 24 32 48 64; do echo $s; for r in {1..4}; do for ((n=0;n<s;n++)); do curl -sF "act=bput" -F "f=@garbage.file" http://127.0.0.1:3923/ 2>&1 & done; wait; echo; done; done; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
##
 | 
					##
 | 
				
			||||||
@@ -36,13 +36,13 @@ for dir in "${dirs[@]}"; do for fn in ふが "$(printf \\xed\\x93)" 'qwe,rty;asd
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
fn=$(printf '\xba\xdc\xab.cab')
 | 
					fn=$(printf '\xba\xdc\xab.cab')
 | 
				
			||||||
echo asdf > "$fn"
 | 
					echo asdf > "$fn"
 | 
				
			||||||
curl --cookie cppwd=wark -sF "act=bput" -F "f=@$fn" http://127.0.0.1:1234/moji/%ED%91/
 | 
					curl --cookie cppwd=wark -sF "act=bput" -F "f=@$fn" http://127.0.0.1:3923/moji/%ED%91/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
##
 | 
					##
 | 
				
			||||||
## test compression
 | 
					## test compression
 | 
				
			||||||
 | 
					
 | 
				
			||||||
wget -S --header='Accept-Encoding: gzip' -U 'MSIE 6.0; SV1' http://127.0.0.1:1234/.cpr/deps/ogv.js -O- | md5sum; p=~ed/dev/copyparty/copyparty/web/deps/ogv.js.gz; md5sum $p; gzip -d < $p | md5sum
 | 
					wget -S --header='Accept-Encoding: gzip' -U 'MSIE 6.0; SV1' http://127.0.0.1:3923/.cpr/deps/ogv.js -O- | md5sum; p=~ed/dev/copyparty/copyparty/web/deps/ogv.js.gz; md5sum $p; gzip -d < $p | md5sum
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
##
 | 
					##
 | 
				
			||||||
@@ -80,3 +80,45 @@ for d in /usr /var; do find $d -type f -size +30M 2>/dev/null; done | while IFS=
 | 
				
			|||||||
# py2 on osx
 | 
					# py2 on osx
 | 
				
			||||||
brew install python@2
 | 
					brew install python@2
 | 
				
			||||||
pip install virtualenv
 | 
					pip install virtualenv
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					##
 | 
				
			||||||
 | 
					## http 206
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# az = abcdefghijklmnopqrstuvwxyz
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					printf '%s\r\n' 'GET /az HTTP/1.1' 'Host: ocv.me' 'Range: bytes=5-10' '' | ncat ocv.me 80 
 | 
				
			||||||
 | 
					# Content-Range: bytes 5-10/26
 | 
				
			||||||
 | 
					# Content-Length: 6
 | 
				
			||||||
 | 
					# fghijk
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Range: bytes=0-1    "ab" Content-Range: bytes 0-1/26
 | 
				
			||||||
 | 
					Range: bytes=24-24  "y"  Content-Range: bytes 24-24/26
 | 
				
			||||||
 | 
					Range: bytes=24-25  "yz" Content-Range: bytes 24-25/26
 | 
				
			||||||
 | 
					Range: bytes=24-    "yz" Content-Range: bytes 24-25/26
 | 
				
			||||||
 | 
					Range: bytes=25-29  "z"  Content-Range: bytes 25-25/26
 | 
				
			||||||
 | 
					Range: bytes=26-         Content-Range: bytes */26
 | 
				
			||||||
 | 
					  HTTP/1.1 416 Requested Range Not Satisfiable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					##
 | 
				
			||||||
 | 
					## md perf
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var tsh = [];
 | 
				
			||||||
 | 
					function convert_markdown(md_text, dest_dom) {
 | 
				
			||||||
 | 
					    tsh.push(new Date().getTime());
 | 
				
			||||||
 | 
					    while (tsh.length > 10)
 | 
				
			||||||
 | 
					        tsh.shift();
 | 
				
			||||||
 | 
					    if (tsh.length > 1) {
 | 
				
			||||||
 | 
					        var end = tsh.slice(-2);
 | 
				
			||||||
 | 
					        console.log("render", end.pop() - end.pop(), (tsh[tsh.length - 1] - tsh[0]) / (tsh.length - 1));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					##
 | 
				
			||||||
 | 
					## tmpfiles.d meme
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					mk() { rm -rf /tmp/foo; sudo -u ed bash -c 'mkdir /tmp/foo; echo hi > /tmp/foo/bar'; }
 | 
				
			||||||
 | 
					mk && t0="$(date)" && while true; do date -s "$(date '+ 1 hour')"; systemd-tmpfiles --clean; ls -1 /tmp | grep foo || break; done; echo "$t0"
 | 
				
			||||||
 | 
					mk && sudo -u ed flock /tmp/foo sleep 40 & sleep 1; ps aux | grep -E 'sleep 40$' && t0="$(date)" && for n in {1..40}; do date -s "$(date '+ 1 day')"; systemd-tmpfiles --clean; ls -1 /tmp | grep foo || break; done; echo "$t0"
 | 
				
			||||||
 | 
					mk && t0="$(date)" && for n in {1..40}; do date -s "$(date '+ 1 day')"; systemd-tmpfiles --clean; ls -1 /tmp | grep foo || break; tar -cf/dev/null /tmp/foo; done; echo "$t0"
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										35
									
								
								docs/pretend-youre-qnap.patch
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								docs/pretend-youre-qnap.patch
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,35 @@
 | 
				
			|||||||
 | 
					diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py
 | 
				
			||||||
 | 
					index 2d3c1ad..e1e85a0 100644
 | 
				
			||||||
 | 
					--- a/copyparty/httpcli.py
 | 
				
			||||||
 | 
					+++ b/copyparty/httpcli.py
 | 
				
			||||||
 | 
					@@ -864,6 +864,30 @@ class HttpCli(object):
 | 
				
			||||||
 | 
					         #
 | 
				
			||||||
 | 
					         # send reply
 | 
				
			||||||
 | 
					 
 | 
				
			||||||
 | 
					+        try:
 | 
				
			||||||
 | 
					+            fakefn = self.conn.hsrv.fakefn
 | 
				
			||||||
 | 
					+            fakectr = self.conn.hsrv.fakectr
 | 
				
			||||||
 | 
					+            fakedata = self.conn.hsrv.fakedata
 | 
				
			||||||
 | 
					+        except:
 | 
				
			||||||
 | 
					+            fakefn = b''
 | 
				
			||||||
 | 
					+            fakectr = 0
 | 
				
			||||||
 | 
					+            fakedata = b''
 | 
				
			||||||
 | 
					+        
 | 
				
			||||||
 | 
					+        self.log('\n{} {}\n{}'.format(fakefn, fakectr, open_args[0]))
 | 
				
			||||||
 | 
					+        if fakefn == open_args[0] and fakectr > 0:
 | 
				
			||||||
 | 
					+            self.reply(fakedata, mime=guess_mime(req_path)[0])
 | 
				
			||||||
 | 
					+            self.conn.hsrv.fakectr = fakectr - 1
 | 
				
			||||||
 | 
					+        else:
 | 
				
			||||||
 | 
					+            with open_func(*open_args) as f:
 | 
				
			||||||
 | 
					+                fakedata = f.read()
 | 
				
			||||||
 | 
					+            
 | 
				
			||||||
 | 
					+            self.conn.hsrv.fakefn = open_args[0]
 | 
				
			||||||
 | 
					+            self.conn.hsrv.fakedata = fakedata
 | 
				
			||||||
 | 
					+            self.conn.hsrv.fakectr = 15
 | 
				
			||||||
 | 
					+            self.reply(fakedata, mime=guess_mime(req_path)[0])
 | 
				
			||||||
 | 
					+        
 | 
				
			||||||
 | 
					+        return True
 | 
				
			||||||
 | 
					+
 | 
				
			||||||
 | 
					         self.out_headers["Accept-Ranges"] = "bytes"
 | 
				
			||||||
 | 
					         self.send_headers(
 | 
				
			||||||
 | 
					             length=upper - lower,
 | 
				
			||||||
							
								
								
									
										62
									
								
								docs/rclone.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								docs/rclone.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,62 @@
 | 
				
			|||||||
 | 
					# using rclone to mount a remote copyparty server as a local filesystem
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					speed estimates with server and client on the same win10 machine:
 | 
				
			||||||
 | 
					* `1070 MiB/s` with rclone as both server and client
 | 
				
			||||||
 | 
					* `570 MiB/s` with rclone-client and `copyparty -ed -j16` as server
 | 
				
			||||||
 | 
					* `220 MiB/s` with rclone-client and `copyparty -ed` as server
 | 
				
			||||||
 | 
					* `100 MiB/s` with [../bin/copyparty-fuse.py](../bin/copyparty-fuse.py) as client
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					when server is on another machine (1gbit LAN),
 | 
				
			||||||
 | 
					* `75 MiB/s` with [../bin/copyparty-fuse.py](../bin/copyparty-fuse.py) as client
 | 
				
			||||||
 | 
					* `92 MiB/s` with rclone-client and `copyparty -ed` as server
 | 
				
			||||||
 | 
					* `103 MiB/s` (connection max) with `copyparty -ed -j16` and all the others
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# creating the config file
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if you want to use password auth, add `headers = Cookie,cppwd=fgsfds` below
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### on windows clients:
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					(
 | 
				
			||||||
 | 
					echo [cpp]
 | 
				
			||||||
 | 
					echo type = http
 | 
				
			||||||
 | 
					echo url = http://127.0.0.1:3923/
 | 
				
			||||||
 | 
					) > %userprofile%\.config\rclone\rclone.conf
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					also install the windows dependencies: [winfsp](https://github.com/billziss-gh/winfsp/releases/latest)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### on unix clients:
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					cat > ~/.config/rclone/rclone.conf <<'EOF'
 | 
				
			||||||
 | 
					[cpp]
 | 
				
			||||||
 | 
					type = http
 | 
				
			||||||
 | 
					url = http://127.0.0.1:3923/
 | 
				
			||||||
 | 
					EOF
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# mounting the copyparty server locally
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					rclone.exe mount --vfs-cache-max-age 5s --attr-timeout 5s --dir-cache-time 5s cpp: Z:
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# use rclone as server too, replacing copyparty
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					feels out of place but is too good not to mention
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					rclone.exe serve http --read-only .
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					* `webdav` gives write-access but `http` is twice as fast
 | 
				
			||||||
 | 
					* `ftp` is buggy, avoid
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# bugs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					* rclone-client throws an exception if you try to read an empty file (should return zero bytes)
 | 
				
			||||||
							
								
								
									
										10
									
								
								docs/unirange.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								docs/unirange.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
				
			|||||||
 | 
					v = "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
 | 
				
			||||||
 | 
					for v in v.split(","):
 | 
				
			||||||
 | 
					    if "+" in v:
 | 
				
			||||||
 | 
					        v = v.split("+")[1]
 | 
				
			||||||
 | 
					    if "-" in v:
 | 
				
			||||||
 | 
					        lo, hi = v.split("-")
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        lo = hi = v
 | 
				
			||||||
 | 
					    for v in range(int(lo, 16), int(hi, 16) + 1):
 | 
				
			||||||
 | 
					        print("{:4x} [{}]".format(v, chr(v)))
 | 
				
			||||||
@@ -3,7 +3,7 @@ WORKDIR /z
 | 
				
			|||||||
ENV     ver_asmcrypto=2821dd1dedd1196c378f5854037dda5c869313f3 \
 | 
					ENV     ver_asmcrypto=2821dd1dedd1196c378f5854037dda5c869313f3 \
 | 
				
			||||||
        ver_markdownit=10.0.0 \
 | 
					        ver_markdownit=10.0.0 \
 | 
				
			||||||
        ver_showdown=1.9.1 \
 | 
					        ver_showdown=1.9.1 \
 | 
				
			||||||
        ver_marked=1.0.0 \
 | 
					        ver_marked=1.1.0 \
 | 
				
			||||||
        ver_ogvjs=1.6.1 \
 | 
					        ver_ogvjs=1.6.1 \
 | 
				
			||||||
        ver_mde=2.10.1 \
 | 
					        ver_mde=2.10.1 \
 | 
				
			||||||
        ver_codemirror=5.53.2 \
 | 
					        ver_codemirror=5.53.2 \
 | 
				
			||||||
@@ -11,8 +11,11 @@ ENV     ver_asmcrypto=2821dd1dedd1196c378f5854037dda5c869313f3 \
 | 
				
			|||||||
        ver_zopfli=1.0.3
 | 
					        ver_zopfli=1.0.3
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# download
 | 
					# download;
 | 
				
			||||||
RUN     apk add cmake make g++ git bash npm patch wget tar pigz brotli gzip unzip python3 python3-dev \
 | 
					# the scp url is latin from https://fonts.googleapis.com/css2?family=Source+Code+Pro&display=swap
 | 
				
			||||||
 | 
					RUN     mkdir -p /z/dist/no-pk \
 | 
				
			||||||
 | 
					        && wget https://fonts.gstatic.com/s/sourcecodepro/v11/HI_SiYsKILxRpg3hIP6sJ7fM7PqlPevW.woff2 -O scp.woff2 \
 | 
				
			||||||
 | 
					        && apk add cmake make g++ git bash npm patch wget tar pigz brotli gzip unzip python3 python3-dev brotli py3-brotli \
 | 
				
			||||||
        && wget https://github.com/brion/ogv.js/releases/download/$ver_ogvjs/ogvjs-$ver_ogvjs.zip -O ogvjs.zip \
 | 
					        && wget https://github.com/brion/ogv.js/releases/download/$ver_ogvjs/ogvjs-$ver_ogvjs.zip -O ogvjs.zip \
 | 
				
			||||||
        && wget https://github.com/asmcrypto/asmcrypto.js/archive/$ver_asmcrypto.tar.gz -O asmcrypto.tgz \
 | 
					        && wget https://github.com/asmcrypto/asmcrypto.js/archive/$ver_asmcrypto.tar.gz -O asmcrypto.tgz \
 | 
				
			||||||
        && wget https://github.com/markedjs/marked/archive/v$ver_marked.tar.gz -O marked.tgz \
 | 
					        && wget https://github.com/markedjs/marked/archive/v$ver_marked.tar.gz -O marked.tgz \
 | 
				
			||||||
@@ -36,23 +39,7 @@ RUN     apk add cmake make g++ git bash npm patch wget tar pigz brotli gzip unzi
 | 
				
			|||||||
            && npm install \
 | 
					            && npm install \
 | 
				
			||||||
            && npm i gulp-cli -g ) \
 | 
					            && npm i gulp-cli -g ) \
 | 
				
			||||||
        && unzip fontawesome.zip \
 | 
					        && unzip fontawesome.zip \
 | 
				
			||||||
        && tar -xf zopfli.tgz \
 | 
					        && tar -xf zopfli.tgz
 | 
				
			||||||
        && mkdir -p /z/dist/no-pk
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# uncomment if you wanna test the abandoned markdown converters
 | 
					 | 
				
			||||||
#ENV     build_abandoned=1
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
RUN     [ $build_abandoned ] || exit 0; \
 | 
					 | 
				
			||||||
        git clone --depth 1 --branch $ver_showdown https://github.com/showdownjs/showdown/ \
 | 
					 | 
				
			||||||
        && wget https://github.com/markdown-it/markdown-it/archive/$ver_markdownit.tar.gz -O markdownit.tgz \
 | 
					 | 
				
			||||||
        && (cd showdown \
 | 
					 | 
				
			||||||
            && npm install \
 | 
					 | 
				
			||||||
            && npm i grunt -g ) \
 | 
					 | 
				
			||||||
        && (tar -xf markdownit.tgz \
 | 
					 | 
				
			||||||
            && cd markdown-it-$ver_markdownit \
 | 
					 | 
				
			||||||
            && npm install )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# build fonttools (which needs zopfli)
 | 
					# build fonttools (which needs zopfli)
 | 
				
			||||||
@@ -80,31 +67,27 @@ RUN     cd ogvjs-$ver_ogvjs \
 | 
				
			|||||||
        && cp -pv \
 | 
					        && cp -pv \
 | 
				
			||||||
            ogv.js \
 | 
					            ogv.js \
 | 
				
			||||||
            ogv-worker-audio.js \
 | 
					            ogv-worker-audio.js \
 | 
				
			||||||
            ogv-demuxer-ogg.js \
 | 
					 | 
				
			||||||
            ogv-demuxer-ogg-wasm.js \
 | 
					            ogv-demuxer-ogg-wasm.js \
 | 
				
			||||||
            ogv-demuxer-ogg-wasm.wasm \
 | 
					            ogv-demuxer-ogg-wasm.wasm \
 | 
				
			||||||
            ogv-demuxer-webm.js \
 | 
					 | 
				
			||||||
            ogv-demuxer-webm-wasm.js \
 | 
					            ogv-demuxer-webm-wasm.js \
 | 
				
			||||||
            ogv-demuxer-webm-wasm.wasm \
 | 
					            ogv-demuxer-webm-wasm.wasm \
 | 
				
			||||||
            ogv-decoder-audio-opus.js \
 | 
					 | 
				
			||||||
            ogv-decoder-audio-opus-wasm.js \
 | 
					            ogv-decoder-audio-opus-wasm.js \
 | 
				
			||||||
            ogv-decoder-audio-opus-wasm.wasm \
 | 
					            ogv-decoder-audio-opus-wasm.wasm \
 | 
				
			||||||
            ogv-decoder-audio-vorbis.js \
 | 
					 | 
				
			||||||
            ogv-decoder-audio-vorbis-wasm.js \
 | 
					            ogv-decoder-audio-vorbis-wasm.js \
 | 
				
			||||||
            ogv-decoder-audio-vorbis-wasm.wasm \
 | 
					            ogv-decoder-audio-vorbis-wasm.wasm \
 | 
				
			||||||
            dynamicaudio.swf \
 | 
					 | 
				
			||||||
            /z/dist
 | 
					            /z/dist
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#            ogv-demuxer-ogg.js \
 | 
				
			||||||
 | 
					#            ogv-demuxer-webm.js \
 | 
				
			||||||
 | 
					#            ogv-decoder-audio-opus.js \
 | 
				
			||||||
 | 
					#            ogv-decoder-audio-vorbis.js \
 | 
				
			||||||
 | 
					#            dynamicaudio.swf \
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# build marked
 | 
					# build marked
 | 
				
			||||||
RUN     wget https://github.com/markedjs/marked/commit/5c166d4164791f643693478e4ac094d63d6e0c9a.patch -O marked-git-1.patch \
 | 
					 | 
				
			||||||
        && wget https://patch-diff.githubusercontent.com/raw/markedjs/marked/pull/1652.patch -O marked-git-2.patch
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
COPY    marked.patch /z/
 | 
					COPY    marked.patch /z/
 | 
				
			||||||
COPY    marked-ln.patch /z/
 | 
					COPY    marked-ln.patch /z/
 | 
				
			||||||
RUN     cd marked-$ver_marked \
 | 
					RUN     cd marked-$ver_marked \
 | 
				
			||||||
        && patch -p1 < /z/marked-git-1.patch \
 | 
					 | 
				
			||||||
        && patch -p1 < /z/marked-git-2.patch \
 | 
					 | 
				
			||||||
        && patch -p1 < /z/marked-ln.patch \
 | 
					        && patch -p1 < /z/marked-ln.patch \
 | 
				
			||||||
        && patch -p1 < /z/marked.patch \
 | 
					        && patch -p1 < /z/marked.patch \
 | 
				
			||||||
        && npm run build \
 | 
					        && npm run build \
 | 
				
			||||||
@@ -138,57 +121,10 @@ RUN     cd easy-markdown-editor-$ver_mde \
 | 
				
			|||||||
        && patch -p1 < /z/easymde-ln.patch \
 | 
					        && patch -p1 < /z/easymde-ln.patch \
 | 
				
			||||||
        && gulp \
 | 
					        && gulp \
 | 
				
			||||||
        && cp -pv dist/easymde.min.css /z/dist/easymde.css \
 | 
					        && cp -pv dist/easymde.min.css /z/dist/easymde.css \
 | 
				
			||||||
        && cp -pv dist/easymde.min.js /z/dist/easymde.js \
 | 
					        && cp -pv dist/easymde.min.js /z/dist/easymde.js
 | 
				
			||||||
        && sed -ri '/pipe.terser/d; /cleanCSS/d' gulpfile.js \
 | 
					 | 
				
			||||||
        && gulp \
 | 
					 | 
				
			||||||
        && cp -pv dist/easymde.min.css /z/dist/easymde.full.css \
 | 
					 | 
				
			||||||
        && cp -pv dist/easymde.min.js /z/dist/easymde.full.js
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# build showdown (abandoned; disabled by default)
 | 
					# build fontawesome and scp
 | 
				
			||||||
COPY    showdown.patch /z/
 | 
					 | 
				
			||||||
RUN     [ $build_abandoned ] || exit 0; \
 | 
					 | 
				
			||||||
        cd showdown \
 | 
					 | 
				
			||||||
        && rm -rf bin dist \
 | 
					 | 
				
			||||||
#       # remove ellipsis plugin \
 | 
					 | 
				
			||||||
        && rm \
 | 
					 | 
				
			||||||
            src/subParsers/ellipsis.js \
 | 
					 | 
				
			||||||
            test/cases/ellipsis* \
 | 
					 | 
				
			||||||
#       # remove html-to-md converter \
 | 
					 | 
				
			||||||
        && rm \
 | 
					 | 
				
			||||||
            test/node/testsuite.makemd.js \
 | 
					 | 
				
			||||||
            test/node/showdown.Converter.makeMarkdown.js \
 | 
					 | 
				
			||||||
#       # remove emojis \
 | 
					 | 
				
			||||||
        && rm src/subParsers/emoji.js \
 | 
					 | 
				
			||||||
        && awk '/^showdown.helper.emojis/ {o=1} !o; /^\}/ {o=0}' \
 | 
					 | 
				
			||||||
            >f <src/helpers.js \
 | 
					 | 
				
			||||||
        && mv f src/helpers.js \
 | 
					 | 
				
			||||||
        && rm -rf test/features/emojis \
 | 
					 | 
				
			||||||
#       # remove ghmentions \
 | 
					 | 
				
			||||||
        && rm test/features/ghMentions.* \
 | 
					 | 
				
			||||||
#       # remove option descriptions \
 | 
					 | 
				
			||||||
        && sed -ri '/descri(ption|be): /d' src/options.js \
 | 
					 | 
				
			||||||
        && patch -p1 < /z/showdown.patch
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
RUN     [ $build_abandoned ] || exit 0; \
 | 
					 | 
				
			||||||
        cd showdown \
 | 
					 | 
				
			||||||
        && grunt build \
 | 
					 | 
				
			||||||
        && sed -ri '/sourceMappingURL=showdown.min.js.map/d' dist/showdown.min.js \
 | 
					 | 
				
			||||||
        && mv dist/showdown.min.js /z/dist/showdown.js \
 | 
					 | 
				
			||||||
        && ls -al /z/dist/showdown.js
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# build markdownit (abandoned; disabled by default)
 | 
					 | 
				
			||||||
COPY    markdown-it.patch /z/
 | 
					 | 
				
			||||||
RUN     [ $build_abandoned ] || exit 0; \
 | 
					 | 
				
			||||||
        cd markdown-it-$ver_markdownit \
 | 
					 | 
				
			||||||
        && patch -p1 < /z/markdown-it.patch \
 | 
					 | 
				
			||||||
        && make browserify \
 | 
					 | 
				
			||||||
        && cp -pv dist/markdown-it.min.js /z/dist/markdown-it.js \
 | 
					 | 
				
			||||||
        && cp -pv dist/markdown-it.js /z/dist/markdown-it-full.js
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# build fontawesome
 | 
					 | 
				
			||||||
COPY    mini-fa.sh /z
 | 
					COPY    mini-fa.sh /z
 | 
				
			||||||
COPY    mini-fa.css /z
 | 
					COPY    mini-fa.css /z
 | 
				
			||||||
RUN     /bin/ash /z/mini-fa.sh
 | 
					RUN     /bin/ash /z/mini-fa.sh
 | 
				
			||||||
@@ -203,38 +139,6 @@ RUN     cd /z/dist \
 | 
				
			|||||||
        && rmdir no-pk
 | 
					        && rmdir no-pk
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# showdown: abandoned due to code-blocks in lists failing
 | 
					# git diff -U2 --no-index marked-1.1.0-orig/ marked-1.1.0-edit/ -U2 | sed -r '/^index /d;s`^(diff --git a/)[^/]+/(.* b/)[^/]+/`\1\2`; s`^(---|\+\+\+) ([ab]/)[^/]+/`\1 \2`' > ../dev/copyparty/scripts/deps-docker/marked-ln.patch
 | 
				
			||||||
# 22770 orig
 | 
					# d=/home/ed/dev/copyparty/scripts/deps-docker/; tar -cf ../x . && ssh root@$bip "cd $d && tar -xv >&2 && make >&2 && tar -cC ../../copyparty/web deps" <../x | (cd ../../copyparty/web/; cat > the.tgz; tar -xvf the.tgz; rm the.tgz)
 | 
				
			||||||
# 12154 no-emojis
 | 
					 | 
				
			||||||
# 12134 no-srcmap
 | 
					 | 
				
			||||||
# 11189 no-descriptions
 | 
					 | 
				
			||||||
# 11152 no-ellipsis
 | 
					 | 
				
			||||||
# 10617 no-this.makeMd
 | 
					 | 
				
			||||||
#  9569 no-extensions
 | 
					 | 
				
			||||||
#  9537 no-extensions
 | 
					 | 
				
			||||||
#  9410 no-mentions
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# markdown-it: abandoned because no header anchors (and too big)
 | 
					 | 
				
			||||||
#       32322 107754 orig (wowee)
 | 
					 | 
				
			||||||
# 19619 21392  71540 less entities
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# marked:
 | 
					 | 
				
			||||||
# 9253 29773 orig
 | 
					 | 
				
			||||||
# 9159 29633 no copyright (reverted)
 | 
					 | 
				
			||||||
# 9040 29057 no sanitize
 | 
					 | 
				
			||||||
# 8870 28631 no email-mangle
 | 
					 | 
				
			||||||
# so really not worth it, just drop the patch when that stops working
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# easymde:
 | 
					 | 
				
			||||||
# 91836 orig
 | 
					 | 
				
			||||||
# 88635 no spellcheck
 | 
					 | 
				
			||||||
# 88392 no urlRE
 | 
					 | 
				
			||||||
# 85651 less bidi
 | 
					 | 
				
			||||||
# 82855 less mode meta
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# d=/home/ed/dev/copyparty/scripts/deps-docker/; tar -cf ../x . && ssh root@$bip "cd $d && tar -xv >&2 && make >&2 && tar -cC ../../copyparty/web deps" <../x | (cd ../../copyparty/web/; cat > the.tgz; tar -xvf the.tgz)
 | 
					 | 
				
			||||||
# gzip -dkf ../dev/copyparty/copyparty/web/deps/deps/marked.full.js.gz && diff -NarU2 ../dev/copyparty/copyparty/web/deps/{,deps/}marked.full.js
 | 
					# gzip -dkf ../dev/copyparty/copyparty/web/deps/deps/marked.full.js.gz && diff -NarU2 ../dev/copyparty/copyparty/web/deps/{,deps/}marked.full.js
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -180,7 +180,7 @@ diff --git a/src/Parser.js b/src/Parser.js
 | 
				
			|||||||
+            // similar to tables, writing contents before the <ul> tag
 | 
					+            // similar to tables, writing contents before the <ul> tag
 | 
				
			||||||
+            // so update the tag attribute as we go
 | 
					+            // so update the tag attribute as we go
 | 
				
			||||||
+            // (assuming all list entries got tagged with a source-line, probably safe w)
 | 
					+            // (assuming all list entries got tagged with a source-line, probably safe w)
 | 
				
			||||||
+            body += this.renderer.tag_ln(item.tokens[0].ln).listitem(itemBody, task, checked);
 | 
					+            body += this.renderer.tag_ln((item.tokens[0] || token).ln).listitem(itemBody, task, checked);
 | 
				
			||||||
           }
 | 
					           }
 | 
				
			||||||
 
 | 
					 
 | 
				
			||||||
-          out += this.renderer.list(body, ordered, start);
 | 
					-          out += this.renderer.list(body, ordered, start);
 | 
				
			||||||
@@ -234,7 +234,7 @@ diff --git a/src/Renderer.js b/src/Renderer.js
 | 
				
			|||||||
-      return '<pre><code>'
 | 
					-      return '<pre><code>'
 | 
				
			||||||
+      return '<pre' + this.ln + '><code>'
 | 
					+      return '<pre' + this.ln + '><code>'
 | 
				
			||||||
         + (escaped ? code : escape(code, true))
 | 
					         + (escaped ? code : escape(code, true))
 | 
				
			||||||
         + '</code></pre>';
 | 
					         + '</code></pre>\n';
 | 
				
			||||||
     }
 | 
					     }
 | 
				
			||||||
 
 | 
					 
 | 
				
			||||||
-    return '<pre><code class="'
 | 
					-    return '<pre><code class="'
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,141 @@
 | 
				
			|||||||
diff -NarU1 marked-1.0.0-orig/src/defaults.js marked-1.0.0-edit/src/defaults.js
 | 
					diff --git a/src/Lexer.js b/src/Lexer.js
 | 
				
			||||||
--- marked-1.0.0-orig/src/defaults.js	2020-04-21 01:03:48.000000000 +0000
 | 
					--- a/src/Lexer.js
 | 
				
			||||||
+++ marked-1.0.0-edit/src/defaults.js	2020-04-25 19:16:56.124621393 +0000
 | 
					+++ b/src/Lexer.js
 | 
				
			||||||
@@ -9,10 +9,6 @@
 | 
					@@ -5,5 +5,5 @@ const { block, inline } = require('./rules.js');
 | 
				
			||||||
 | 
					 /**
 | 
				
			||||||
 | 
					  * smartypants text replacement
 | 
				
			||||||
 | 
					- */
 | 
				
			||||||
 | 
					+ *
 | 
				
			||||||
 | 
					 function smartypants(text) {
 | 
				
			||||||
 | 
					   return text
 | 
				
			||||||
 | 
					@@ -26,5 +26,5 @@ function smartypants(text) {
 | 
				
			||||||
 | 
					 /**
 | 
				
			||||||
 | 
					  * mangle email addresses
 | 
				
			||||||
 | 
					- */
 | 
				
			||||||
 | 
					+ *
 | 
				
			||||||
 | 
					 function mangle(text) {
 | 
				
			||||||
 | 
					   let out = '',
 | 
				
			||||||
 | 
					@@ -439,5 +439,5 @@ module.exports = class Lexer {
 | 
				
			||||||
 | 
					 
 | 
				
			||||||
 | 
					       // autolink
 | 
				
			||||||
 | 
					-      if (token = this.tokenizer.autolink(src, mangle)) {
 | 
				
			||||||
 | 
					+      if (token = this.tokenizer.autolink(src)) {
 | 
				
			||||||
 | 
					         src = src.substring(token.raw.length);
 | 
				
			||||||
 | 
					         tokens.push(token);
 | 
				
			||||||
 | 
					@@ -446,5 +446,5 @@ module.exports = class Lexer {
 | 
				
			||||||
 | 
					 
 | 
				
			||||||
 | 
					       // url (gfm)
 | 
				
			||||||
 | 
					-      if (!inLink && (token = this.tokenizer.url(src, mangle))) {
 | 
				
			||||||
 | 
					+      if (!inLink && (token = this.tokenizer.url(src))) {
 | 
				
			||||||
 | 
					         src = src.substring(token.raw.length);
 | 
				
			||||||
 | 
					         tokens.push(token);
 | 
				
			||||||
 | 
					@@ -453,5 +453,5 @@ module.exports = class Lexer {
 | 
				
			||||||
 | 
					 
 | 
				
			||||||
 | 
					       // text
 | 
				
			||||||
 | 
					-      if (token = this.tokenizer.inlineText(src, inRawBlock, smartypants)) {
 | 
				
			||||||
 | 
					+      if (token = this.tokenizer.inlineText(src, inRawBlock)) {
 | 
				
			||||||
 | 
					         src = src.substring(token.raw.length);
 | 
				
			||||||
 | 
					         tokens.push(token);
 | 
				
			||||||
 | 
					diff --git a/src/Renderer.js b/src/Renderer.js
 | 
				
			||||||
 | 
					--- a/src/Renderer.js
 | 
				
			||||||
 | 
					+++ b/src/Renderer.js
 | 
				
			||||||
 | 
					@@ -140,5 +140,5 @@ module.exports = class Renderer {
 | 
				
			||||||
 | 
					 
 | 
				
			||||||
 | 
					   link(href, title, text) {
 | 
				
			||||||
 | 
					-    href = cleanUrl(this.options.sanitize, this.options.baseUrl, href);
 | 
				
			||||||
 | 
					+    href = cleanUrl(this.options.baseUrl, href);
 | 
				
			||||||
 | 
					     if (href === null) {
 | 
				
			||||||
 | 
					       return text;
 | 
				
			||||||
 | 
					@@ -153,5 +153,5 @@ module.exports = class Renderer {
 | 
				
			||||||
 | 
					 
 | 
				
			||||||
 | 
					   image(href, title, text) {
 | 
				
			||||||
 | 
					-    href = cleanUrl(this.options.sanitize, this.options.baseUrl, href);
 | 
				
			||||||
 | 
					+    href = cleanUrl(this.options.baseUrl, href);
 | 
				
			||||||
 | 
					     if (href === null) {
 | 
				
			||||||
 | 
					       return text;
 | 
				
			||||||
 | 
					diff --git a/src/Tokenizer.js b/src/Tokenizer.js
 | 
				
			||||||
 | 
					--- a/src/Tokenizer.js
 | 
				
			||||||
 | 
					+++ b/src/Tokenizer.js
 | 
				
			||||||
 | 
					@@ -287,11 +287,8 @@ module.exports = class Tokenizer {
 | 
				
			||||||
 | 
					     if (cap) {
 | 
				
			||||||
 | 
					       return {
 | 
				
			||||||
 | 
					-        type: this.options.sanitize
 | 
				
			||||||
 | 
					-          ? 'paragraph'
 | 
				
			||||||
 | 
					-          : 'html',
 | 
				
			||||||
 | 
					+        type: 'html',
 | 
				
			||||||
 | 
					         raw: cap[0],
 | 
				
			||||||
 | 
					-        pre: !this.options.sanitizer
 | 
				
			||||||
 | 
					-          && (cap[1] === 'pre' || cap[1] === 'script' || cap[1] === 'style'),
 | 
				
			||||||
 | 
					-        text: this.options.sanitize ? (this.options.sanitizer ? this.options.sanitizer(cap[0]) : escape(cap[0])) : cap[0]
 | 
				
			||||||
 | 
					+        pre: cap[1] === 'pre' || cap[1] === 'script' || cap[1] === 'style',
 | 
				
			||||||
 | 
					+        text: cap[0]
 | 
				
			||||||
 | 
					       };
 | 
				
			||||||
 | 
					     }
 | 
				
			||||||
 | 
					@@ -421,15 +418,9 @@ module.exports = class Tokenizer {
 | 
				
			||||||
 | 
					 
 | 
				
			||||||
 | 
					       return {
 | 
				
			||||||
 | 
					-        type: this.options.sanitize
 | 
				
			||||||
 | 
					-          ? 'text'
 | 
				
			||||||
 | 
					-          : 'html',
 | 
				
			||||||
 | 
					+        type: 'html',
 | 
				
			||||||
 | 
					         raw: cap[0],
 | 
				
			||||||
 | 
					         inLink,
 | 
				
			||||||
 | 
					         inRawBlock,
 | 
				
			||||||
 | 
					-        text: this.options.sanitize
 | 
				
			||||||
 | 
					-          ? (this.options.sanitizer
 | 
				
			||||||
 | 
					-            ? this.options.sanitizer(cap[0])
 | 
				
			||||||
 | 
					-            : escape(cap[0]))
 | 
				
			||||||
 | 
					-          : cap[0]
 | 
				
			||||||
 | 
					+        text: cap[0]
 | 
				
			||||||
 | 
					       };
 | 
				
			||||||
 | 
					     }
 | 
				
			||||||
 | 
					@@ -550,10 +541,10 @@ module.exports = class Tokenizer {
 | 
				
			||||||
 | 
					   }
 | 
				
			||||||
 | 
					 
 | 
				
			||||||
 | 
					-  autolink(src, mangle) {
 | 
				
			||||||
 | 
					+  autolink(src) {
 | 
				
			||||||
 | 
					     const cap = this.rules.inline.autolink.exec(src);
 | 
				
			||||||
 | 
					     if (cap) {
 | 
				
			||||||
 | 
					       let text, href;
 | 
				
			||||||
 | 
					       if (cap[2] === '@') {
 | 
				
			||||||
 | 
					-        text = escape(this.options.mangle ? mangle(cap[1]) : cap[1]);
 | 
				
			||||||
 | 
					+        text = escape(cap[1]);
 | 
				
			||||||
 | 
					         href = 'mailto:' + text;
 | 
				
			||||||
 | 
					       } else {
 | 
				
			||||||
 | 
					@@ -578,10 +569,10 @@ module.exports = class Tokenizer {
 | 
				
			||||||
 | 
					   }
 | 
				
			||||||
 | 
					 
 | 
				
			||||||
 | 
					-  url(src, mangle) {
 | 
				
			||||||
 | 
					+  url(src) {
 | 
				
			||||||
 | 
					     let cap;
 | 
				
			||||||
 | 
					     if (cap = this.rules.inline.url.exec(src)) {
 | 
				
			||||||
 | 
					       let text, href;
 | 
				
			||||||
 | 
					       if (cap[2] === '@') {
 | 
				
			||||||
 | 
					-        text = escape(this.options.mangle ? mangle(cap[0]) : cap[0]);
 | 
				
			||||||
 | 
					+        text = escape(cap[0]);
 | 
				
			||||||
 | 
					         href = 'mailto:' + text;
 | 
				
			||||||
 | 
					       } else {
 | 
				
			||||||
 | 
					@@ -615,12 +606,12 @@ module.exports = class Tokenizer {
 | 
				
			||||||
 | 
					   }
 | 
				
			||||||
 | 
					 
 | 
				
			||||||
 | 
					-  inlineText(src, inRawBlock, smartypants) {
 | 
				
			||||||
 | 
					+  inlineText(src, inRawBlock) {
 | 
				
			||||||
 | 
					     const cap = this.rules.inline.text.exec(src);
 | 
				
			||||||
 | 
					     if (cap) {
 | 
				
			||||||
 | 
					       let text;
 | 
				
			||||||
 | 
					       if (inRawBlock) {
 | 
				
			||||||
 | 
					-        text = this.options.sanitize ? (this.options.sanitizer ? this.options.sanitizer(cap[0]) : escape(cap[0])) : cap[0];
 | 
				
			||||||
 | 
					+        text = cap[0];
 | 
				
			||||||
 | 
					       } else {
 | 
				
			||||||
 | 
					-        text = escape(this.options.smartypants ? smartypants(cap[0]) : cap[0]);
 | 
				
			||||||
 | 
					+        text = escape(cap[0]);
 | 
				
			||||||
 | 
					       }
 | 
				
			||||||
 | 
					       return {
 | 
				
			||||||
 | 
					diff --git a/src/defaults.js b/src/defaults.js
 | 
				
			||||||
 | 
					--- a/src/defaults.js
 | 
				
			||||||
 | 
					+++ b/src/defaults.js
 | 
				
			||||||
 | 
					@@ -8,12 +8,8 @@ function getDefaults() {
 | 
				
			||||||
 | 
					     highlight: null,
 | 
				
			||||||
     langPrefix: 'language-',
 | 
					     langPrefix: 'language-',
 | 
				
			||||||
-    mangle: true,
 | 
					-    mangle: true,
 | 
				
			||||||
     pedantic: false,
 | 
					     pedantic: false,
 | 
				
			||||||
@@ -12,10 +146,12 @@ diff -NarU1 marked-1.0.0-orig/src/defaults.js marked-1.0.0-edit/src/defaults.js
 | 
				
			|||||||
     smartLists: false,
 | 
					     smartLists: false,
 | 
				
			||||||
-    smartypants: false,
 | 
					-    smartypants: false,
 | 
				
			||||||
     tokenizer: null,
 | 
					     tokenizer: null,
 | 
				
			||||||
diff -NarU1 marked-1.0.0-orig/src/helpers.js marked-1.0.0-edit/src/helpers.js
 | 
					     walkTokens: null,
 | 
				
			||||||
--- marked-1.0.0-orig/src/helpers.js	2020-04-21 01:03:48.000000000 +0000
 | 
					diff --git a/src/helpers.js b/src/helpers.js
 | 
				
			||||||
+++ marked-1.0.0-edit/src/helpers.js	2020-04-25 18:58:43.001320210 +0000
 | 
					--- a/src/helpers.js
 | 
				
			||||||
@@ -65,16 +65,3 @@
 | 
					+++ b/src/helpers.js
 | 
				
			||||||
 | 
					@@ -64,18 +64,5 @@ function edit(regex, opt) {
 | 
				
			||||||
 | 
					 const nonWordAndColonTest = /[^\w:]/g;
 | 
				
			||||||
 const originIndependentUrl = /^$|^[a-z][a-z0-9+.-]*:|^[?#]/i;
 | 
					 const originIndependentUrl = /^$|^[a-z][a-z0-9+.-]*:|^[?#]/i;
 | 
				
			||||||
-function cleanUrl(sanitize, base, href) {
 | 
					-function cleanUrl(sanitize, base, href) {
 | 
				
			||||||
-  if (sanitize) {
 | 
					-  if (sanitize) {
 | 
				
			||||||
@@ -33,7 +169,9 @@ diff -NarU1 marked-1.0.0-orig/src/helpers.js marked-1.0.0-edit/src/helpers.js
 | 
				
			|||||||
-  }
 | 
					-  }
 | 
				
			||||||
+function cleanUrl(base, href) {
 | 
					+function cleanUrl(base, href) {
 | 
				
			||||||
   if (base && !originIndependentUrl.test(href)) {
 | 
					   if (base && !originIndependentUrl.test(href)) {
 | 
				
			||||||
@@ -224,8 +211,2 @@
 | 
					     href = resolveUrl(base, href);
 | 
				
			||||||
 | 
					@@ -223,10 +210,4 @@ function findClosingBracket(str, b) {
 | 
				
			||||||
 | 
					 }
 | 
				
			||||||
 
 | 
					 
 | 
				
			||||||
-function checkSanitizeDeprecation(opt) {
 | 
					-function checkSanitizeDeprecation(opt) {
 | 
				
			||||||
-  if (opt && opt.sanitize && !opt.silent) {
 | 
					-  if (opt && opt.sanitize && !opt.silent) {
 | 
				
			||||||
@@ -42,228 +180,161 @@ diff -NarU1 marked-1.0.0-orig/src/helpers.js marked-1.0.0-edit/src/helpers.js
 | 
				
			|||||||
-}
 | 
					-}
 | 
				
			||||||
-
 | 
					-
 | 
				
			||||||
 module.exports = {
 | 
					 module.exports = {
 | 
				
			||||||
@@ -240,4 +221,3 @@
 | 
					   escape,
 | 
				
			||||||
 | 
					@@ -239,5 +220,4 @@ module.exports = {
 | 
				
			||||||
 | 
					   splitCells,
 | 
				
			||||||
   rtrim,
 | 
					   rtrim,
 | 
				
			||||||
-  findClosingBracket,
 | 
					-  findClosingBracket,
 | 
				
			||||||
-  checkSanitizeDeprecation
 | 
					-  checkSanitizeDeprecation
 | 
				
			||||||
+  findClosingBracket
 | 
					+  findClosingBracket
 | 
				
			||||||
 };
 | 
					 };
 | 
				
			||||||
diff -NarU1 marked-1.0.0-orig/src/Lexer.js marked-1.0.0-edit/src/Lexer.js
 | 
					diff --git a/src/marked.js b/src/marked.js
 | 
				
			||||||
--- marked-1.0.0-orig/src/Lexer.js	2020-04-21 01:03:48.000000000 +0000
 | 
					--- a/src/marked.js
 | 
				
			||||||
+++ marked-1.0.0-edit/src/Lexer.js	2020-04-25 22:46:54.107584066 +0000
 | 
					+++ b/src/marked.js
 | 
				
			||||||
@@ -6,3 +6,3 @@
 | 
					@@ -7,5 +7,4 @@ const Slugger = require('./Slugger.js');
 | 
				
			||||||
  * smartypants text replacement
 | 
					 const {
 | 
				
			||||||
- */
 | 
					 | 
				
			||||||
+ *
 | 
					 | 
				
			||||||
 function smartypants(text) {
 | 
					 | 
				
			||||||
@@ -27,3 +27,3 @@
 | 
					 | 
				
			||||||
  * mangle email addresses
 | 
					 | 
				
			||||||
- */
 | 
					 | 
				
			||||||
+ *
 | 
					 | 
				
			||||||
 function mangle(text) {
 | 
					 | 
				
			||||||
@@ -388,3 +388,3 @@
 | 
					 | 
				
			||||||
       // autolink
 | 
					 | 
				
			||||||
-      if (token = this.tokenizer.autolink(src, mangle)) {
 | 
					 | 
				
			||||||
+      if (token = this.tokenizer.autolink(src)) {
 | 
					 | 
				
			||||||
         src = src.substring(token.raw.length);
 | 
					 | 
				
			||||||
@@ -395,3 +395,3 @@
 | 
					 | 
				
			||||||
       // url (gfm)
 | 
					 | 
				
			||||||
-      if (!inLink && (token = this.tokenizer.url(src, mangle))) {
 | 
					 | 
				
			||||||
+      if (!inLink && (token = this.tokenizer.url(src))) {
 | 
					 | 
				
			||||||
         src = src.substring(token.raw.length);
 | 
					 | 
				
			||||||
@@ -402,3 +402,3 @@
 | 
					 | 
				
			||||||
       // text
 | 
					 | 
				
			||||||
-      if (token = this.tokenizer.inlineText(src, inRawBlock, smartypants)) {
 | 
					 | 
				
			||||||
+      if (token = this.tokenizer.inlineText(src, inRawBlock)) {
 | 
					 | 
				
			||||||
         src = src.substring(token.raw.length);
 | 
					 | 
				
			||||||
diff -NarU1 marked-1.0.0-orig/src/marked.js marked-1.0.0-edit/src/marked.js
 | 
					 | 
				
			||||||
--- marked-1.0.0-orig/src/marked.js	2020-04-21 01:03:48.000000000 +0000
 | 
					 | 
				
			||||||
+++ marked-1.0.0-edit/src/marked.js	2020-04-25 22:42:55.140924439 +0000
 | 
					 | 
				
			||||||
@@ -8,3 +8,2 @@
 | 
					 | 
				
			||||||
   merge,
 | 
					   merge,
 | 
				
			||||||
-  checkSanitizeDeprecation,
 | 
					-  checkSanitizeDeprecation,
 | 
				
			||||||
   escape
 | 
					   escape
 | 
				
			||||||
@@ -37,3 +36,2 @@
 | 
					 } = require('./helpers.js');
 | 
				
			||||||
     opt = merge({}, marked.defaults, opt || {});
 | 
					@@ -35,5 +34,4 @@ function marked(src, opt, callback) {
 | 
				
			||||||
-    checkSanitizeDeprecation(opt);
 | 
					 
 | 
				
			||||||
     const highlight = opt.highlight;
 | 
					   opt = merge({}, marked.defaults, opt || {});
 | 
				
			||||||
@@ -101,6 +99,5 @@
 | 
					-  checkSanitizeDeprecation(opt);
 | 
				
			||||||
     opt = merge({}, marked.defaults, opt || {});
 | 
					 
 | 
				
			||||||
-    checkSanitizeDeprecation(opt);
 | 
					   if (callback) {
 | 
				
			||||||
     return Parser.parse(Lexer.lex(src, opt), opt);
 | 
					@@ -108,5 +106,5 @@ function marked(src, opt, callback) {
 | 
				
			||||||
 | 
					     return Parser.parse(tokens, opt);
 | 
				
			||||||
   } catch (e) {
 | 
					   } catch (e) {
 | 
				
			||||||
-    e.message += '\nPlease report this to https://github.com/markedjs/marked.';
 | 
					-    e.message += '\nPlease report this to https://github.com/markedjs/marked.';
 | 
				
			||||||
+    e.message += '\nmake issue @ https://github.com/9001/copyparty';
 | 
					+    e.message += '\nmake issue @ https://github.com/9001/copyparty';
 | 
				
			||||||
     if ((opt || marked.defaults).silent) {
 | 
					     if (opt.silent) {
 | 
				
			||||||
diff -NarU1 marked-1.0.0-orig/src/Renderer.js marked-1.0.0-edit/src/Renderer.js
 | 
					       return '<p>An error occurred:</p><pre>'
 | 
				
			||||||
--- marked-1.0.0-orig/src/Renderer.js	2020-04-21 01:03:48.000000000 +0000
 | 
					diff --git a/test/bench.js b/test/bench.js
 | 
				
			||||||
+++ marked-1.0.0-edit/src/Renderer.js	2020-04-25 18:59:15.091319265 +0000
 | 
					--- a/test/bench.js
 | 
				
			||||||
@@ -134,3 +134,3 @@
 | 
					+++ b/test/bench.js
 | 
				
			||||||
   link(href, title, text) {
 | 
					@@ -33,5 +33,4 @@ async function runBench(options) {
 | 
				
			||||||
-    href = cleanUrl(this.options.sanitize, this.options.baseUrl, href);
 | 
					     breaks: false,
 | 
				
			||||||
+    href = cleanUrl(this.options.baseUrl, href);
 | 
					 | 
				
			||||||
     if (href === null) {
 | 
					 | 
				
			||||||
@@ -147,3 +147,3 @@
 | 
					 | 
				
			||||||
   image(href, title, text) {
 | 
					 | 
				
			||||||
-    href = cleanUrl(this.options.sanitize, this.options.baseUrl, href);
 | 
					 | 
				
			||||||
+    href = cleanUrl(this.options.baseUrl, href);
 | 
					 | 
				
			||||||
     if (href === null) {
 | 
					 | 
				
			||||||
diff -NarU1 marked-1.0.0-orig/src/Tokenizer.js marked-1.0.0-edit/src/Tokenizer.js
 | 
					 | 
				
			||||||
--- marked-1.0.0-orig/src/Tokenizer.js	2020-04-21 01:03:48.000000000 +0000
 | 
					 | 
				
			||||||
+++ marked-1.0.0-edit/src/Tokenizer.js	2020-04-25 22:47:07.610917004 +0000
 | 
					 | 
				
			||||||
@@ -256,9 +256,6 @@
 | 
					 | 
				
			||||||
       return {
 | 
					 | 
				
			||||||
-        type: this.options.sanitize
 | 
					 | 
				
			||||||
-          ? 'paragraph'
 | 
					 | 
				
			||||||
-          : 'html',
 | 
					 | 
				
			||||||
-        raw: cap[0],
 | 
					 | 
				
			||||||
-        pre: !this.options.sanitizer
 | 
					 | 
				
			||||||
-          && (cap[1] === 'pre' || cap[1] === 'script' || cap[1] === 'style'),
 | 
					 | 
				
			||||||
-        text: this.options.sanitize ? (this.options.sanitizer ? this.options.sanitizer(cap[0]) : escape(cap[0])) : cap[0]
 | 
					 | 
				
			||||||
+        type: 'html',
 | 
					 | 
				
			||||||
+        raw: cap[0],
 | 
					 | 
				
			||||||
+        pre: cap[1] === 'pre' || cap[1] === 'script' || cap[1] === 'style',
 | 
					 | 
				
			||||||
+        text: cap[0]
 | 
					 | 
				
			||||||
       };
 | 
					 | 
				
			||||||
@@ -382,5 +379,3 @@
 | 
					 | 
				
			||||||
       return {
 | 
					 | 
				
			||||||
-        type: this.options.sanitize
 | 
					 | 
				
			||||||
-          ? 'text'
 | 
					 | 
				
			||||||
-          : 'html',
 | 
					 | 
				
			||||||
+        type: 'html',
 | 
					 | 
				
			||||||
         raw: cap[0],
 | 
					 | 
				
			||||||
@@ -388,7 +383,3 @@
 | 
					 | 
				
			||||||
         inRawBlock,
 | 
					 | 
				
			||||||
-        text: this.options.sanitize
 | 
					 | 
				
			||||||
-          ? (this.options.sanitizer
 | 
					 | 
				
			||||||
-            ? this.options.sanitizer(cap[0])
 | 
					 | 
				
			||||||
-            : escape(cap[0]))
 | 
					 | 
				
			||||||
-          : cap[0]
 | 
					 | 
				
			||||||
+        text: cap[0]
 | 
					 | 
				
			||||||
       };
 | 
					 | 
				
			||||||
@@ -504,3 +495,3 @@
 | 
					 | 
				
			||||||
 
 | 
					 | 
				
			||||||
-  autolink(src, mangle) {
 | 
					 | 
				
			||||||
+  autolink(src) {
 | 
					 | 
				
			||||||
     const cap = this.rules.inline.autolink.exec(src);
 | 
					 | 
				
			||||||
@@ -509,3 +500,3 @@
 | 
					 | 
				
			||||||
       if (cap[2] === '@') {
 | 
					 | 
				
			||||||
-        text = escape(this.options.mangle ? mangle(cap[1]) : cap[1]);
 | 
					 | 
				
			||||||
+        text = escape(cap[1]);
 | 
					 | 
				
			||||||
         href = 'mailto:' + text;
 | 
					 | 
				
			||||||
@@ -532,3 +523,3 @@
 | 
					 | 
				
			||||||
 
 | 
					 | 
				
			||||||
-  url(src, mangle) {
 | 
					 | 
				
			||||||
+  url(src) {
 | 
					 | 
				
			||||||
     let cap;
 | 
					 | 
				
			||||||
@@ -537,3 +528,3 @@
 | 
					 | 
				
			||||||
       if (cap[2] === '@') {
 | 
					 | 
				
			||||||
-        text = escape(this.options.mangle ? mangle(cap[0]) : cap[0]);
 | 
					 | 
				
			||||||
+        text = escape(cap[0]);
 | 
					 | 
				
			||||||
         href = 'mailto:' + text;
 | 
					 | 
				
			||||||
@@ -569,3 +560,3 @@
 | 
					 | 
				
			||||||
 
 | 
					 | 
				
			||||||
-  inlineText(src, inRawBlock, smartypants) {
 | 
					 | 
				
			||||||
+  inlineText(src, inRawBlock) {
 | 
					 | 
				
			||||||
     const cap = this.rules.inline.text.exec(src);
 | 
					 | 
				
			||||||
@@ -574,5 +565,5 @@
 | 
					 | 
				
			||||||
       if (inRawBlock) {
 | 
					 | 
				
			||||||
-        text = this.options.sanitize ? (this.options.sanitizer ? this.options.sanitizer(cap[0]) : escape(cap[0])) : cap[0];
 | 
					 | 
				
			||||||
+        text = cap[0];
 | 
					 | 
				
			||||||
       } else {
 | 
					 | 
				
			||||||
-        text = escape(this.options.smartypants ? smartypants(cap[0]) : cap[0]);
 | 
					 | 
				
			||||||
+        text = escape(cap[0]);
 | 
					 | 
				
			||||||
       }
 | 
					 | 
				
			||||||
diff -NarU1 marked-1.0.0-orig/test/bench.js marked-1.0.0-edit/test/bench.js
 | 
					 | 
				
			||||||
--- marked-1.0.0-orig/test/bench.js	2020-04-21 01:03:48.000000000 +0000
 | 
					 | 
				
			||||||
+++ marked-1.0.0-edit/test/bench.js	2020-04-25 19:02:27.227980287 +0000
 | 
					 | 
				
			||||||
@@ -34,3 +34,2 @@
 | 
					 | 
				
			||||||
     pedantic: false,
 | 
					     pedantic: false,
 | 
				
			||||||
-    sanitize: false,
 | 
					-    sanitize: false,
 | 
				
			||||||
     smartLists: false
 | 
					     smartLists: false
 | 
				
			||||||
@@ -46,3 +45,2 @@
 | 
					   });
 | 
				
			||||||
 | 
					@@ -45,5 +44,4 @@ async function runBench(options) {
 | 
				
			||||||
 | 
					     breaks: false,
 | 
				
			||||||
     pedantic: false,
 | 
					     pedantic: false,
 | 
				
			||||||
-    sanitize: false,
 | 
					-    sanitize: false,
 | 
				
			||||||
     smartLists: false
 | 
					     smartLists: false
 | 
				
			||||||
@@ -59,3 +57,2 @@
 | 
					   });
 | 
				
			||||||
 | 
					@@ -58,5 +56,4 @@ async function runBench(options) {
 | 
				
			||||||
 | 
					     breaks: false,
 | 
				
			||||||
     pedantic: false,
 | 
					     pedantic: false,
 | 
				
			||||||
-    sanitize: false,
 | 
					-    sanitize: false,
 | 
				
			||||||
     smartLists: false
 | 
					     smartLists: false
 | 
				
			||||||
@@ -71,3 +68,2 @@
 | 
					   });
 | 
				
			||||||
 | 
					@@ -70,5 +67,4 @@ async function runBench(options) {
 | 
				
			||||||
 | 
					     breaks: false,
 | 
				
			||||||
     pedantic: false,
 | 
					     pedantic: false,
 | 
				
			||||||
-    sanitize: false,
 | 
					-    sanitize: false,
 | 
				
			||||||
     smartLists: false
 | 
					     smartLists: false
 | 
				
			||||||
@@ -84,3 +80,2 @@
 | 
					   });
 | 
				
			||||||
 | 
					@@ -83,5 +79,4 @@ async function runBench(options) {
 | 
				
			||||||
 | 
					     breaks: false,
 | 
				
			||||||
     pedantic: true,
 | 
					     pedantic: true,
 | 
				
			||||||
-    sanitize: false,
 | 
					-    sanitize: false,
 | 
				
			||||||
     smartLists: false
 | 
					     smartLists: false
 | 
				
			||||||
@@ -96,3 +91,2 @@
 | 
					   });
 | 
				
			||||||
 | 
					@@ -95,5 +90,4 @@ async function runBench(options) {
 | 
				
			||||||
 | 
					     breaks: false,
 | 
				
			||||||
     pedantic: true,
 | 
					     pedantic: true,
 | 
				
			||||||
-    sanitize: false,
 | 
					-    sanitize: false,
 | 
				
			||||||
     smartLists: false
 | 
					     smartLists: false
 | 
				
			||||||
diff -NarU1 marked-1.0.0-orig/test/specs/run-spec.js marked-1.0.0-edit/test/specs/run-spec.js
 | 
					   });
 | 
				
			||||||
--- marked-1.0.0-orig/test/specs/run-spec.js	2020-04-21 01:03:48.000000000 +0000
 | 
					diff --git a/test/specs/run-spec.js b/test/specs/run-spec.js
 | 
				
			||||||
+++ marked-1.0.0-edit/test/specs/run-spec.js	2020-04-25 19:05:24.321308408 +0000
 | 
					--- a/test/specs/run-spec.js
 | 
				
			||||||
@@ -21,6 +21,2 @@
 | 
					+++ b/test/specs/run-spec.js
 | 
				
			||||||
 | 
					@@ -22,8 +22,4 @@ function runSpecs(title, dir, showCompletionTable, options) {
 | 
				
			||||||
           }
 | 
					           }
 | 
				
			||||||
 | 
					 
 | 
				
			||||||
-          if (spec.options.sanitizer) {
 | 
					-          if (spec.options.sanitizer) {
 | 
				
			||||||
-            // eslint-disable-next-line no-eval
 | 
					-            // eslint-disable-next-line no-eval
 | 
				
			||||||
-            spec.options.sanitizer = eval(spec.options.sanitizer);
 | 
					-            spec.options.sanitizer = eval(spec.options.sanitizer);
 | 
				
			||||||
-          }
 | 
					-          }
 | 
				
			||||||
 | 
					 
 | 
				
			||||||
           (spec.only ? fit : (spec.skip ? xit : it))('should ' + passFail + example, async() => {
 | 
					           (spec.only ? fit : (spec.skip ? xit : it))('should ' + passFail + example, async() => {
 | 
				
			||||||
@@ -49,2 +45 @@
 | 
					@@ -53,3 +49,2 @@ runSpecs('Original', './original', false, { gfm: false, pedantic: true });
 | 
				
			||||||
 | 
					 runSpecs('New', './new');
 | 
				
			||||||
 runSpecs('ReDOS', './redos');
 | 
					 runSpecs('ReDOS', './redos');
 | 
				
			||||||
-runSpecs('Security', './security', false, { silent: true }); // silent - do not show deprecation warning
 | 
					-runSpecs('Security', './security', false, { silent: true }); // silent - do not show deprecation warning
 | 
				
			||||||
diff -NarU1 marked-1.0.0-orig/test/unit/Lexer-spec.js marked-1.0.0-edit/test/unit/Lexer-spec.js
 | 
					diff --git a/test/unit/Lexer-spec.js b/test/unit/Lexer-spec.js
 | 
				
			||||||
--- marked-1.0.0-orig/test/unit/Lexer-spec.js	2020-04-21 01:03:48.000000000 +0000
 | 
					--- a/test/unit/Lexer-spec.js
 | 
				
			||||||
+++ marked-1.0.0-edit/test/unit/Lexer-spec.js	2020-04-25 22:47:27.170916427 +0000
 | 
					+++ b/test/unit/Lexer-spec.js
 | 
				
			||||||
@@ -464,3 +464,3 @@
 | 
					@@ -465,5 +465,5 @@ a | b
 | 
				
			||||||
 | 
					     });
 | 
				
			||||||
 
 | 
					 
 | 
				
			||||||
-    it('sanitize', () => {
 | 
					-    it('sanitize', () => {
 | 
				
			||||||
+    /*it('sanitize', () => {
 | 
					+    /*it('sanitize', () => {
 | 
				
			||||||
       expectTokens({
 | 
					       expectTokens({
 | 
				
			||||||
@@ -482,3 +482,3 @@
 | 
					         md: '<div>html</div>',
 | 
				
			||||||
 | 
					@@ -483,5 +483,5 @@ a | b
 | 
				
			||||||
 | 
					         ]
 | 
				
			||||||
       });
 | 
					       });
 | 
				
			||||||
-    });
 | 
					-    });
 | 
				
			||||||
+    });*/
 | 
					+    });*/
 | 
				
			||||||
   });
 | 
					   });
 | 
				
			||||||
@@ -586,3 +586,3 @@
 | 
					 
 | 
				
			||||||
 | 
					@@ -587,5 +587,5 @@ a | b
 | 
				
			||||||
 | 
					       });
 | 
				
			||||||
 
 | 
					 
 | 
				
			||||||
-      it('html sanitize', () => {
 | 
					-      it('html sanitize', () => {
 | 
				
			||||||
+      /*it('html sanitize', () => {
 | 
					+      /*it('html sanitize', () => {
 | 
				
			||||||
         expectInlineTokens({
 | 
					         expectInlineTokens({
 | 
				
			||||||
@@ -596,3 +596,3 @@
 | 
					           md: '<div>html</div>',
 | 
				
			||||||
 | 
					@@ -597,5 +597,5 @@ a | b
 | 
				
			||||||
 | 
					           ]
 | 
				
			||||||
         });
 | 
					         });
 | 
				
			||||||
-      });
 | 
					-      });
 | 
				
			||||||
+      });*/
 | 
					+      });*/
 | 
				
			||||||
 
 | 
					 
 | 
				
			||||||
@@ -825,3 +825,3 @@
 | 
					       it('link', () => {
 | 
				
			||||||
 | 
					@@ -909,5 +909,5 @@ a | b
 | 
				
			||||||
 | 
					         });
 | 
				
			||||||
 
 | 
					 
 | 
				
			||||||
-        it('autolink mangle email', () => {
 | 
					-        it('autolink mangle email', () => {
 | 
				
			||||||
+        /*it('autolink mangle email', () => {
 | 
					+        /*it('autolink mangle email', () => {
 | 
				
			||||||
           expectInlineTokens({
 | 
					           expectInlineTokens({
 | 
				
			||||||
@@ -845,3 +845,3 @@
 | 
					             md: '<test@example.com>',
 | 
				
			||||||
 | 
					@@ -929,5 +929,5 @@ a | b
 | 
				
			||||||
 | 
					             ]
 | 
				
			||||||
           });
 | 
					           });
 | 
				
			||||||
-        });
 | 
					-        });
 | 
				
			||||||
+        });*/
 | 
					+        });*/
 | 
				
			||||||
 
 | 
					 
 | 
				
			||||||
@@ -882,3 +882,3 @@
 | 
					         it('url', () => {
 | 
				
			||||||
 | 
					@@ -966,5 +966,5 @@ a | b
 | 
				
			||||||
 | 
					         });
 | 
				
			||||||
 
 | 
					 
 | 
				
			||||||
-        it('url mangle email', () => {
 | 
					-        it('url mangle email', () => {
 | 
				
			||||||
+        /*it('url mangle email', () => {
 | 
					+        /*it('url mangle email', () => {
 | 
				
			||||||
           expectInlineTokens({
 | 
					           expectInlineTokens({
 | 
				
			||||||
@@ -902,3 +902,3 @@
 | 
					             md: 'test@example.com',
 | 
				
			||||||
 | 
					@@ -986,5 +986,5 @@ a | b
 | 
				
			||||||
 | 
					             ]
 | 
				
			||||||
           });
 | 
					           });
 | 
				
			||||||
-        });
 | 
					-        });
 | 
				
			||||||
+        });*/
 | 
					+        });*/
 | 
				
			||||||
       });
 | 
					       });
 | 
				
			||||||
@@ -918,3 +918,3 @@
 | 
					 
 | 
				
			||||||
 | 
					@@ -1002,5 +1002,5 @@ a | b
 | 
				
			||||||
 | 
					       });
 | 
				
			||||||
 
 | 
					 
 | 
				
			||||||
-      describe('smartypants', () => {
 | 
					-      describe('smartypants', () => {
 | 
				
			||||||
+      /*describe('smartypants', () => {
 | 
					+      /*describe('smartypants', () => {
 | 
				
			||||||
         it('single quotes', () => {
 | 
					         it('single quotes', () => {
 | 
				
			||||||
@@ -988,3 +988,3 @@
 | 
					           expectInlineTokens({
 | 
				
			||||||
 | 
					@@ -1072,5 +1072,5 @@ a | b
 | 
				
			||||||
 | 
					           });
 | 
				
			||||||
         });
 | 
					         });
 | 
				
			||||||
-      });
 | 
					-      });
 | 
				
			||||||
+      });*/
 | 
					+      });*/
 | 
				
			||||||
     });
 | 
					     });
 | 
				
			||||||
 | 
					   });
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -26,3 +26,6 @@ awk '/:before .content:"\\/ {sub(/[^"]+"./,""); sub(/".*/,""); print}' </z/dist/
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
# and finally create a woff with just our icons
 | 
					# and finally create a woff with just our icons
 | 
				
			||||||
pyftsubset "$orig_woff" --unicodes-file=/z/icon.list --no-ignore-missing-unicodes --flavor=woff --with-zopfli --output-file=/z/dist/no-pk/mini-fa.woff --verbose
 | 
					pyftsubset "$orig_woff" --unicodes-file=/z/icon.list --no-ignore-missing-unicodes --flavor=woff --with-zopfli --output-file=/z/dist/no-pk/mini-fa.woff --verbose
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# scp is easier, just want basic latin
 | 
				
			||||||
 | 
					pyftsubset /z/scp.woff2 --unicodes="20-7e,ab,b7,bb,2022" --no-ignore-missing-unicodes --flavor=woff2 --output-file=/z/dist/no-pk/scp.woff2 --verbose
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										100
									
								
								scripts/fusefuzz.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										100
									
								
								scripts/fusefuzz.py
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,100 @@
 | 
				
			|||||||
 | 
					#!/usr/bin/env python3
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
 | 
					import time
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					mkdir -p /dev/shm/fusefuzz/{r,v}
 | 
				
			||||||
 | 
					PYTHONPATH=.. python3 -m copyparty -v /dev/shm/fusefuzz/r::r -i 127.0.0.1
 | 
				
			||||||
 | 
					../bin/copyparty-fuse.py /dev/shm/fusefuzz/v http://127.0.0.1:3923/ 2 0
 | 
				
			||||||
 | 
					(d="$PWD"; cd /dev/shm/fusefuzz && "$d"/fusefuzz.py)
 | 
				
			||||||
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def chk(fsz, rsz, ofs0, shift, ofs, rf, vf):
 | 
				
			||||||
 | 
					    if ofs != rf.tell():
 | 
				
			||||||
 | 
					        rf.seek(ofs)
 | 
				
			||||||
 | 
					        vf.seek(ofs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    rb = rf.read(rsz)
 | 
				
			||||||
 | 
					    vb = vf.read(rsz)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    print(f"fsz {fsz} rsz {rsz} ofs {ofs0} shift {shift} ofs {ofs} = {len(rb)}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if rb != vb:
 | 
				
			||||||
 | 
					        for n, buf in enumerate([rb, vb]):
 | 
				
			||||||
 | 
					            with open("buf." + str(n), "wb") as f:
 | 
				
			||||||
 | 
					                f.write(buf)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        raise Exception(f"{len(rb)} != {len(vb)}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return rb, vb
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def main():
 | 
				
			||||||
 | 
					    v = "v"
 | 
				
			||||||
 | 
					    for n in range(5):
 | 
				
			||||||
 | 
					        with open(f"r/{n}", "wb") as f:
 | 
				
			||||||
 | 
					            f.write(b"h" * n)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    rand = os.urandom(7919)  # prime
 | 
				
			||||||
 | 
					    for fsz in range(1024 * 1024 * 2 - 3, 1024 * 1024 * 2 + 3):
 | 
				
			||||||
 | 
					        with open("r/f", "wb", fsz) as f:
 | 
				
			||||||
 | 
					            f.write((rand * int(fsz / len(rand) + 1))[:fsz])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for rsz in range(64 * 1024 - 2, 64 * 1024 + 2):
 | 
				
			||||||
 | 
					            ofslist = [0, 1, 2]
 | 
				
			||||||
 | 
					            for n in range(3):
 | 
				
			||||||
 | 
					                ofslist.append(fsz - n)
 | 
				
			||||||
 | 
					                ofslist.append(fsz - (rsz * 1 + n))
 | 
				
			||||||
 | 
					                ofslist.append(fsz - (rsz * 2 + n))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            for ofs0 in ofslist:
 | 
				
			||||||
 | 
					                for shift in range(-3, 3):
 | 
				
			||||||
 | 
					                    print(f"fsz {fsz} rsz {rsz} ofs {ofs0} shift {shift}")
 | 
				
			||||||
 | 
					                    ofs = ofs0
 | 
				
			||||||
 | 
					                    if ofs < 0 or ofs >= fsz:
 | 
				
			||||||
 | 
					                        continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    for n in range(1, 3):
 | 
				
			||||||
 | 
					                        with open(f"{v}/{n}", "rb") as f:
 | 
				
			||||||
 | 
					                            f.read()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    prev_ofs = -99
 | 
				
			||||||
 | 
					                    with open("r/f", "rb", rsz) as rf:
 | 
				
			||||||
 | 
					                        with open(f"{v}/f", "rb", rsz) as vf:
 | 
				
			||||||
 | 
					                            while True:
 | 
				
			||||||
 | 
					                                ofs += shift
 | 
				
			||||||
 | 
					                                if ofs < 0 or ofs > fsz or ofs == prev_ofs:
 | 
				
			||||||
 | 
					                                    break
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                prev_ofs = ofs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                rb, vb = chk(fsz, rsz, ofs0, shift, ofs, rf, vf)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                if not rb:
 | 
				
			||||||
 | 
					                                    break
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                ofs += len(rb)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    for n in range(1, 3):
 | 
				
			||||||
 | 
					                        with open(f"{v}/{n}", "rb") as f:
 | 
				
			||||||
 | 
					                            f.read()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    with open("r/f", "rb", rsz) as rf:
 | 
				
			||||||
 | 
					                        with open(f"{v}/f", "rb", rsz) as vf:
 | 
				
			||||||
 | 
					                            for n in range(2):
 | 
				
			||||||
 | 
					                                ofs += shift
 | 
				
			||||||
 | 
					                                if ofs < 0 or ofs > fsz:
 | 
				
			||||||
 | 
					                                    break
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                rb, vb = chk(fsz, rsz, ofs0, shift, ofs, rf, vf)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                ofs -= rsz
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # bumping fsz, sleep away the dentry cache in cppf
 | 
				
			||||||
 | 
					        time.sleep(1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if __name__ == "__main__":
 | 
				
			||||||
 | 
					    main()
 | 
				
			||||||
@@ -13,6 +13,9 @@ echo
 | 
				
			|||||||
#
 | 
					#
 | 
				
			||||||
# `no-ogv` saves ~500k by removing the opus/vorbis audio codecs
 | 
					# `no-ogv` saves ~500k by removing the opus/vorbis audio codecs
 | 
				
			||||||
#   (only affects apple devices; everything else has native support)
 | 
					#   (only affects apple devices; everything else has native support)
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# `no-cm` saves ~90k by removing easymde/codemirror
 | 
				
			||||||
 | 
					#   (the fancy markdown editor)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
command -v gtar  >/dev/null &&
 | 
					command -v gtar  >/dev/null &&
 | 
				
			||||||
@@ -21,6 +24,7 @@ command -v gfind >/dev/null && {
 | 
				
			|||||||
	sed()  { gsed  "$@"; }
 | 
						sed()  { gsed  "$@"; }
 | 
				
			||||||
	find() { gfind "$@"; }
 | 
						find() { gfind "$@"; }
 | 
				
			||||||
	sort() { gsort "$@"; }
 | 
						sort() { gsort "$@"; }
 | 
				
			||||||
 | 
						unexpand() { gunexpand "$@"; }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[ -e copyparty/__main__.py ] || cd ..
 | 
					[ -e copyparty/__main__.py ] || cd ..
 | 
				
			||||||
@@ -35,9 +39,15 @@ while [ ! -z "$1" ]; do
 | 
				
			|||||||
	[ "$1" = clean  ] && clean=1  && shift && continue
 | 
						[ "$1" = clean  ] && clean=1  && shift && continue
 | 
				
			||||||
	[ "$1" = re     ] && repack=1 && shift && continue
 | 
						[ "$1" = re     ] && repack=1 && shift && continue
 | 
				
			||||||
	[ "$1" = no-ogv ] && no_ogv=1 && shift && continue
 | 
						[ "$1" = no-ogv ] && no_ogv=1 && shift && continue
 | 
				
			||||||
 | 
						[ "$1" = no-cm  ] && no_cm=1  && shift && continue
 | 
				
			||||||
	break
 | 
						break
 | 
				
			||||||
done
 | 
					done
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					tmv() {
 | 
				
			||||||
 | 
						touch -r "$1" t
 | 
				
			||||||
 | 
						mv t "$1"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
rm -rf sfx/*
 | 
					rm -rf sfx/*
 | 
				
			||||||
mkdir -p sfx build
 | 
					mkdir -p sfx build
 | 
				
			||||||
cd sfx
 | 
					cd sfx
 | 
				
			||||||
@@ -62,7 +72,15 @@ cd sfx
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	tar -zxf $f
 | 
						tar -zxf $f
 | 
				
			||||||
	mv Jinja2-*/jinja2 .
 | 
						mv Jinja2-*/jinja2 .
 | 
				
			||||||
	rm -rf Jinja2-* jinja2/testsuite
 | 
						rm -rf Jinja2-* jinja2/testsuite jinja2/_markupsafe/tests.py jinja2/_stringdefs.py
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						f=jinja2/lexer.py
 | 
				
			||||||
 | 
						sed -r '/.*föö.*/    raise SyntaxError/' <$f >t
 | 
				
			||||||
 | 
						tmv $f
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						f=jinja2/_markupsafe/_constants.py
 | 
				
			||||||
 | 
						awk '!/: [0-9]+,?$/ || /(amp|gt|lt|quot|apos|nbsp).:/' <$f >t
 | 
				
			||||||
 | 
						tmv $f
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	# msys2 tar is bad, make the best of it
 | 
						# msys2 tar is bad, make the best of it
 | 
				
			||||||
	echo collecting source
 | 
						echo collecting source
 | 
				
			||||||
@@ -98,10 +116,27 @@ rm -f copyparty/web/deps/*.full.*
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
# it's fine dw
 | 
					# it's fine dw
 | 
				
			||||||
grep -lE '\.full\.(js|css)' copyparty/web/* |
 | 
					grep -lE '\.full\.(js|css)' copyparty/web/* |
 | 
				
			||||||
while IFS= read -r x; do sed -ri 's/\.full\.(js|css)/.\1/g' "$x"; done
 | 
					while IFS= read -r x; do
 | 
				
			||||||
 | 
						sed -r 's/\.full\.(js|css)/.\1/g' <"$x" >t
 | 
				
			||||||
 | 
						tmv "$x"
 | 
				
			||||||
 | 
					done
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[ $no_ogv ] &&
 | 
					[ $no_ogv ] &&
 | 
				
			||||||
	rm -rf copyparty/web/deps/{dynamicaudio,ogv}* copyparty/web/browser.js
 | 
						rm -rf copyparty/web/deps/{dynamicaudio,ogv}*
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[ $no_cm ] && {
 | 
				
			||||||
 | 
						rm -rf copyparty/web/mde.* copyparty/web/deps/easymde*
 | 
				
			||||||
 | 
						echo h > copyparty/web/mde.html
 | 
				
			||||||
 | 
						f=copyparty/web/md.html
 | 
				
			||||||
 | 
						sed -r '/edit2">edit \(fancy/d' <$f >t && tmv "$f"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# up2k goes from 28k to 22k laff
 | 
				
			||||||
 | 
					echo entabbening
 | 
				
			||||||
 | 
					find | grep -E '\.(js|css|html|py)$' | while IFS= read -r f; do
 | 
				
			||||||
 | 
						unexpand -t 4 --first-only <"$f" >t
 | 
				
			||||||
 | 
						tmv "$f"
 | 
				
			||||||
 | 
					done
 | 
				
			||||||
 | 
					
 | 
				
			||||||
echo creating tar
 | 
					echo creating tar
 | 
				
			||||||
args=(--owner=1000 --group=1000)
 | 
					args=(--owner=1000 --group=1000)
 | 
				
			||||||
@@ -132,19 +167,5 @@ printf "done:\n"
 | 
				
			|||||||
printf "  %s\n" "$(realpath $sfx_out)."{sh,py}
 | 
					printf "  %s\n" "$(realpath $sfx_out)."{sh,py}
 | 
				
			||||||
# rm -rf *
 | 
					# rm -rf *
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# -rw-r--r--  1 ed ed 811271 May  5 14:35 tar.bz2
 | 
					# tar -tvf ../sfx/tar | sed -r 's/(.* ....-..-.. ..:.. )(.*)/\2 `` \1/' | sort | sed -r 's/(.*) `` (.*)/\2 \1/'| less
 | 
				
			||||||
# -rw-r--r--  1 ed ed 732016 May  5 14:35 tar.xz
 | 
					# for n in {1..9}; do tar -tf tar | grep -vE '/$' | sed -r 's/(.*)\.(.*)/\2.\1/' | sort | sed -r 's/([^\.]+)\.(.*)/\2.\1/' | tar -cT- | bzip2 -c$n | wc -c; done 
 | 
				
			||||||
 | 
					 | 
				
			||||||
# -rwxr-xr-x  1 ed ed 830425 May  5 14:35 copyparty-sfx.py*
 | 
					 | 
				
			||||||
# -rwxr-xr-x  1 ed ed 734088 May  5 14:35 copyparty-sfx.sh*
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# -rwxr-xr-x  1 ed ed 799690 May  5 14:45 copyparty-sfx.py*
 | 
					 | 
				
			||||||
# -rwxr-xr-x  1 ed ed 735004 May  5 14:45 copyparty-sfx.sh*
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# time pigz -11 -J 34 -I 5730 < tar > tar.gz.5730
 | 
					 | 
				
			||||||
# real	8m50.622s
 | 
					 | 
				
			||||||
# user	33m9.821s
 | 
					 | 
				
			||||||
# -rw-r--r--  1 ed ed 1136640 May  5 14:50 tar
 | 
					 | 
				
			||||||
# -rw-r--r--  1 ed ed  296334 May  5 14:50 tar.bz2
 | 
					 | 
				
			||||||
# -rw-r--r--  1 ed ed  324705 May  5 15:01 tar.gz.5730
 | 
					 | 
				
			||||||
# -rw-r--r--  1 ed ed  257208 May  5 14:50 tar.xz
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,7 +2,7 @@
 | 
				
			|||||||
# coding: utf-8
 | 
					# coding: utf-8
 | 
				
			||||||
from __future__ import print_function, unicode_literals
 | 
					from __future__ import print_function, unicode_literals
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import re, os, sys, stat, time, shutil, tarfile, hashlib, platform, tempfile
 | 
					import re, os, sys, time, shutil, signal, tarfile, hashlib, platform, tempfile
 | 
				
			||||||
import subprocess as sp
 | 
					import subprocess as sp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
@@ -29,6 +29,7 @@ STAMP = None
 | 
				
			|||||||
PY2 = sys.version_info[0] == 2
 | 
					PY2 = sys.version_info[0] == 2
 | 
				
			||||||
sys.dont_write_bytecode = True
 | 
					sys.dont_write_bytecode = True
 | 
				
			||||||
me = os.path.abspath(os.path.realpath(__file__))
 | 
					me = os.path.abspath(os.path.realpath(__file__))
 | 
				
			||||||
 | 
					cpp = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def eprint(*args, **kwargs):
 | 
					def eprint(*args, **kwargs):
 | 
				
			||||||
@@ -191,6 +192,16 @@ def makesfx(tar_src, ver, ts):
 | 
				
			|||||||
# skip 0
 | 
					# skip 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def u8(gen):
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        for s in gen:
 | 
				
			||||||
 | 
					            yield s.decode("utf-8", "ignore")
 | 
				
			||||||
 | 
					    except:
 | 
				
			||||||
 | 
					        yield s
 | 
				
			||||||
 | 
					        for s in gen:
 | 
				
			||||||
 | 
					            yield s
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def get_py_win(ret):
 | 
					def get_py_win(ret):
 | 
				
			||||||
    tops = []
 | 
					    tops = []
 | 
				
			||||||
    p = str(os.getenv("LocalAppdata"))
 | 
					    p = str(os.getenv("LocalAppdata"))
 | 
				
			||||||
@@ -216,11 +227,11 @@ def get_py_win(ret):
 | 
				
			|||||||
    # $WIRESHARK_SLOGAN
 | 
					    # $WIRESHARK_SLOGAN
 | 
				
			||||||
    for top in tops:
 | 
					    for top in tops:
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            for name1 in sorted(os.listdir(top), reverse=True):
 | 
					            for name1 in u8(sorted(os.listdir(top), reverse=True)):
 | 
				
			||||||
                if name1.lower().startswith("python"):
 | 
					                if name1.lower().startswith("python"):
 | 
				
			||||||
                    path1 = os.path.join(top, name1)
 | 
					                    path1 = os.path.join(top, name1)
 | 
				
			||||||
                    try:
 | 
					                    try:
 | 
				
			||||||
                        for name2 in os.listdir(path1):
 | 
					                        for name2 in u8(os.listdir(path1)):
 | 
				
			||||||
                            if name2.lower() == "python.exe":
 | 
					                            if name2.lower() == "python.exe":
 | 
				
			||||||
                                path2 = os.path.join(path1, name2)
 | 
					                                path2 = os.path.join(path1, name2)
 | 
				
			||||||
                                ret[path2.lower()] = path2
 | 
					                                ret[path2.lower()] = path2
 | 
				
			||||||
@@ -237,7 +248,7 @@ def get_py_nix(ret):
 | 
				
			|||||||
            next
 | 
					            next
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            for fn in os.listdir(bindir):
 | 
					            for fn in u8(os.listdir(bindir)):
 | 
				
			||||||
                if ptn.match(fn):
 | 
					                if ptn.match(fn):
 | 
				
			||||||
                    fn = os.path.join(bindir, fn)
 | 
					                    fn = os.path.join(bindir, fn)
 | 
				
			||||||
                    ret[fn.lower()] = fn
 | 
					                    ret[fn.lower()] = fn
 | 
				
			||||||
@@ -260,7 +271,7 @@ def read_py(binp):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
def get_pys():
 | 
					def get_pys():
 | 
				
			||||||
    ver, chk = read_py(sys.executable)
 | 
					    ver, chk = read_py(sys.executable)
 | 
				
			||||||
    if chk:
 | 
					    if chk or PY2:
 | 
				
			||||||
        return [[chk, ver, sys.executable]]
 | 
					        return [[chk, ver, sys.executable]]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    hits = {sys.executable.lower(): sys.executable}
 | 
					    hits = {sys.executable.lower(): sys.executable}
 | 
				
			||||||
@@ -295,17 +306,19 @@ def hashfile(fn):
 | 
				
			|||||||
def unpack():
 | 
					def unpack():
 | 
				
			||||||
    """unpacks the tar yielded by `data`"""
 | 
					    """unpacks the tar yielded by `data`"""
 | 
				
			||||||
    name = "pe-copyparty"
 | 
					    name = "pe-copyparty"
 | 
				
			||||||
 | 
					    tag = "v" + str(STAMP)
 | 
				
			||||||
    withpid = "{}.{}".format(name, os.getpid())
 | 
					    withpid = "{}.{}".format(name, os.getpid())
 | 
				
			||||||
    top = tempfile.gettempdir()
 | 
					    top = tempfile.gettempdir()
 | 
				
			||||||
    final = os.path.join(top, name)
 | 
					    final = os.path.join(top, name)
 | 
				
			||||||
    mine = os.path.join(top, withpid)
 | 
					    mine = os.path.join(top, withpid)
 | 
				
			||||||
    tar = os.path.join(mine, "tar")
 | 
					    tar = os.path.join(mine, "tar")
 | 
				
			||||||
    tag_mine = os.path.join(mine, "v" + str(STAMP))
 | 
					 | 
				
			||||||
    tag_final = os.path.join(final, "v" + str(STAMP))
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if os.path.exists(tag_final):
 | 
					    try:
 | 
				
			||||||
        msg("found early")
 | 
					        if tag in os.listdir(final):
 | 
				
			||||||
        return final
 | 
					            msg("found early")
 | 
				
			||||||
 | 
					            return final
 | 
				
			||||||
 | 
					    except:
 | 
				
			||||||
 | 
					        pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    nwrite = 0
 | 
					    nwrite = 0
 | 
				
			||||||
    os.mkdir(mine)
 | 
					    os.mkdir(mine)
 | 
				
			||||||
@@ -328,12 +341,15 @@ def unpack():
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    os.remove(tar)
 | 
					    os.remove(tar)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    with open(tag_mine, "wb") as f:
 | 
					    with open(os.path.join(mine, tag), "wb") as f:
 | 
				
			||||||
        f.write(b"h\n")
 | 
					        f.write(b"h\n")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if os.path.exists(tag_final):
 | 
					    try:
 | 
				
			||||||
        msg("found late")
 | 
					        if tag in os.listdir(final):
 | 
				
			||||||
        return final
 | 
					            msg("found late")
 | 
				
			||||||
 | 
					            return final
 | 
				
			||||||
 | 
					    except:
 | 
				
			||||||
 | 
					        pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    try:
 | 
					    try:
 | 
				
			||||||
        if os.path.islink(final):
 | 
					        if os.path.islink(final):
 | 
				
			||||||
@@ -352,7 +368,7 @@ def unpack():
 | 
				
			|||||||
            msg("reloc fail,", mine)
 | 
					            msg("reloc fail,", mine)
 | 
				
			||||||
            return mine
 | 
					            return mine
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for fn in os.listdir(top):
 | 
					    for fn in u8(os.listdir(top)):
 | 
				
			||||||
        if fn.startswith(name) and fn not in [name, withpid]:
 | 
					        if fn.startswith(name) and fn not in [name, withpid]:
 | 
				
			||||||
            try:
 | 
					            try:
 | 
				
			||||||
                old = os.path.join(top, fn)
 | 
					                old = os.path.join(top, fn)
 | 
				
			||||||
@@ -418,17 +434,35 @@ def get_payload():
 | 
				
			|||||||
def confirm():
 | 
					def confirm():
 | 
				
			||||||
    msg()
 | 
					    msg()
 | 
				
			||||||
    msg("*** hit enter to exit ***")
 | 
					    msg("*** hit enter to exit ***")
 | 
				
			||||||
    raw_input() if PY2 else input()
 | 
					    try:
 | 
				
			||||||
 | 
					        raw_input() if PY2 else input()
 | 
				
			||||||
 | 
					    except:
 | 
				
			||||||
 | 
					        pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def run(tmp, py):
 | 
					def run(tmp, py):
 | 
				
			||||||
 | 
					    global cpp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    msg("OK")
 | 
					    msg("OK")
 | 
				
			||||||
    msg("will use:", py)
 | 
					    msg("will use:", py)
 | 
				
			||||||
    msg("bound to:", tmp)
 | 
					    msg("bound to:", tmp)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # "systemd-tmpfiles-clean.timer"?? HOW do you even come up with this shit
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        import fcntl
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        fd = os.open(tmp, os.O_RDONLY)
 | 
				
			||||||
 | 
					        fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
 | 
				
			||||||
 | 
					        tmp = os.readlink(tmp)  # can't flock a symlink, even with O_NOFOLLOW
 | 
				
			||||||
 | 
					    except:
 | 
				
			||||||
 | 
					        pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    fp_py = os.path.join(tmp, "py")
 | 
					    fp_py = os.path.join(tmp, "py")
 | 
				
			||||||
    with open(fp_py, "wb") as f:
 | 
					    try:
 | 
				
			||||||
        f.write(py.encode("utf-8") + b"\n")
 | 
					        with open(fp_py, "wb") as f:
 | 
				
			||||||
 | 
					            f.write(py.encode("utf-8") + b"\n")
 | 
				
			||||||
 | 
					    except:
 | 
				
			||||||
 | 
					        pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # avoid loading ./copyparty.py
 | 
					    # avoid loading ./copyparty.py
 | 
				
			||||||
    cmd = [
 | 
					    cmd = [
 | 
				
			||||||
@@ -440,16 +474,21 @@ def run(tmp, py):
 | 
				
			|||||||
    ] + list(sys.argv[1:])
 | 
					    ] + list(sys.argv[1:])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    msg("\n", cmd, "\n")
 | 
					    msg("\n", cmd, "\n")
 | 
				
			||||||
    p = sp.Popen(str(x) for x in cmd)
 | 
					    cpp = sp.Popen(str(x) for x in cmd)
 | 
				
			||||||
    try:
 | 
					    try:
 | 
				
			||||||
        p.wait()
 | 
					        cpp.wait()
 | 
				
			||||||
    except:
 | 
					    except:
 | 
				
			||||||
        p.wait()
 | 
					        cpp.wait()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if p.returncode != 0:
 | 
					    if cpp.returncode != 0:
 | 
				
			||||||
        confirm()
 | 
					        confirm()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    sys.exit(p.returncode)
 | 
					    sys.exit(cpp.returncode)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def bye(sig, frame):
 | 
				
			||||||
 | 
					    if cpp is not None:
 | 
				
			||||||
 | 
					        cpp.terminate()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def main():
 | 
					def main():
 | 
				
			||||||
@@ -484,6 +523,8 @@ def main():
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    # skip 0
 | 
					    # skip 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    signal.signal(signal.SIGTERM, bye)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    tmp = unpack()
 | 
					    tmp = unpack()
 | 
				
			||||||
    fp_py = os.path.join(tmp, "py")
 | 
					    fp_py = os.path.join(tmp, "py")
 | 
				
			||||||
    if os.path.exists(fp_py):
 | 
					    if os.path.exists(fp_py):
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -32,8 +32,12 @@ dir="$(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
# detect available pythons
 | 
					# detect available pythons
 | 
				
			||||||
(IFS=:; for d in $PATH; do
 | 
					(IFS=:; for d in $PATH; do
 | 
				
			||||||
	printf '%s\n' "$d"/python* "$d"/pypy* | tac;
 | 
						printf '%s\n' "$d"/python* "$d"/pypy*;
 | 
				
			||||||
done) | grep -E '(python|pypy)[0-9\.-]*$' > $dir/pys || true
 | 
					done) |
 | 
				
			||||||
 | 
					(sed -E 's/(.*\/[^/0-9]+)([0-9]?[^/]*)$/\2 \1/' || cat) |
 | 
				
			||||||
 | 
					(sort -nr || cat) |
 | 
				
			||||||
 | 
					(sed -E 's/([^ ]*) (.*)/\2\1/' || cat) |
 | 
				
			||||||
 | 
					grep -E '/(python|pypy)[0-9\.-]*$' >$dir/pys || true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# see if we made a choice before
 | 
					# see if we made a choice before
 | 
				
			||||||
[ -z "$pybin" ] && pybin="$(cat $dir/py 2>/dev/null || true)"
 | 
					[ -z "$pybin" ] && pybin="$(cat $dir/py 2>/dev/null || true)"
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										164
									
								
								scripts/speedtest-fs.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										164
									
								
								scripts/speedtest-fs.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,164 @@
 | 
				
			|||||||
 | 
					#!/usr/bin/env python
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
 | 
					import sys
 | 
				
			||||||
 | 
					import stat
 | 
				
			||||||
 | 
					import time
 | 
				
			||||||
 | 
					import signal
 | 
				
			||||||
 | 
					import traceback
 | 
				
			||||||
 | 
					import threading
 | 
				
			||||||
 | 
					from queue import Queue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""speedtest-fs: filesystem performance estimate"""
 | 
				
			||||||
 | 
					__author__ = "ed <copyparty@ocv.me>"
 | 
				
			||||||
 | 
					__copyright__ = 2020
 | 
				
			||||||
 | 
					__license__ = "MIT"
 | 
				
			||||||
 | 
					__url__ = "https://github.com/9001/copyparty/"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_spd(nbyte, nsec):
 | 
				
			||||||
 | 
					    if not nsec:
 | 
				
			||||||
 | 
					        return "0.000 MB   0.000 sec   0.000 MB/s"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    mb = nbyte / (1024 * 1024.0)
 | 
				
			||||||
 | 
					    spd = mb / nsec
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return f"{mb:.3f} MB   {nsec:.3f} sec   {spd:.3f} MB/s"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Inf(object):
 | 
				
			||||||
 | 
					    def __init__(self, t0):
 | 
				
			||||||
 | 
					        self.msgs = []
 | 
				
			||||||
 | 
					        self.errors = []
 | 
				
			||||||
 | 
					        self.reports = []
 | 
				
			||||||
 | 
					        self.mtx_msgs = threading.Lock()
 | 
				
			||||||
 | 
					        self.mtx_reports = threading.Lock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.n_byte = 0
 | 
				
			||||||
 | 
					        self.n_sec = 0
 | 
				
			||||||
 | 
					        self.n_done = 0
 | 
				
			||||||
 | 
					        self.t0 = t0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        thr = threading.Thread(target=self.print_msgs)
 | 
				
			||||||
 | 
					        thr.daemon = True
 | 
				
			||||||
 | 
					        thr.start()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def msg(self, fn, n_read):
 | 
				
			||||||
 | 
					        with self.mtx_msgs:
 | 
				
			||||||
 | 
					            self.msgs.append(f"{fn} {n_read}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def err(self, fn):
 | 
				
			||||||
 | 
					        with self.mtx_reports:
 | 
				
			||||||
 | 
					            self.errors.append(f"{fn}\n{traceback.format_exc()}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def print_msgs(self):
 | 
				
			||||||
 | 
					        while True:
 | 
				
			||||||
 | 
					            time.sleep(0.02)
 | 
				
			||||||
 | 
					            with self.mtx_msgs:
 | 
				
			||||||
 | 
					                msgs = self.msgs
 | 
				
			||||||
 | 
					                self.msgs = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if not msgs:
 | 
				
			||||||
 | 
					                continue
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            msgs = msgs[-64:]
 | 
				
			||||||
 | 
					            msgs = [f"{get_spd(self.n_byte, self.n_sec)}   {x}" for x in msgs]
 | 
				
			||||||
 | 
					            print("\n".join(msgs))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def report(self, fn, n_byte, n_sec):
 | 
				
			||||||
 | 
					        with self.mtx_reports:
 | 
				
			||||||
 | 
					            self.reports.append([n_byte, n_sec, fn])
 | 
				
			||||||
 | 
					            self.n_byte += n_byte
 | 
				
			||||||
 | 
					            self.n_sec += n_sec
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def done(self):
 | 
				
			||||||
 | 
					        with self.mtx_reports:
 | 
				
			||||||
 | 
					            self.n_done += 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_files(dir_path):
 | 
				
			||||||
 | 
					    for fn in os.listdir(dir_path):
 | 
				
			||||||
 | 
					        fn = os.path.join(dir_path, fn)
 | 
				
			||||||
 | 
					        st = os.stat(fn).st_mode
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if stat.S_ISDIR(st):
 | 
				
			||||||
 | 
					            yield from get_files(fn)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if stat.S_ISREG(st):
 | 
				
			||||||
 | 
					            yield fn
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def worker(q, inf, read_sz):
 | 
				
			||||||
 | 
					    while True:
 | 
				
			||||||
 | 
					        fn = q.get()
 | 
				
			||||||
 | 
					        if not fn:
 | 
				
			||||||
 | 
					            break
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        n_read = 0
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            t0 = time.time()
 | 
				
			||||||
 | 
					            with open(fn, "rb") as f:
 | 
				
			||||||
 | 
					                while True:
 | 
				
			||||||
 | 
					                    buf = f.read(read_sz)
 | 
				
			||||||
 | 
					                    if not buf:
 | 
				
			||||||
 | 
					                        break
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    n_read += len(buf)
 | 
				
			||||||
 | 
					                    inf.msg(fn, n_read)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            inf.report(fn, n_read, time.time() - t0)
 | 
				
			||||||
 | 
					        except:
 | 
				
			||||||
 | 
					            inf.err(fn)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    inf.done()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def sighandler(signo, frame):
 | 
				
			||||||
 | 
					    os._exit(0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def main():
 | 
				
			||||||
 | 
					    signal.signal(signal.SIGINT, sighandler)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    root = "."
 | 
				
			||||||
 | 
					    if len(sys.argv) > 1:
 | 
				
			||||||
 | 
					        root = sys.argv[1]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    t0 = time.time()
 | 
				
			||||||
 | 
					    q = Queue(256)
 | 
				
			||||||
 | 
					    inf = Inf(t0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    num_threads = 8
 | 
				
			||||||
 | 
					    read_sz = 32 * 1024
 | 
				
			||||||
 | 
					    for _ in range(num_threads):
 | 
				
			||||||
 | 
					        thr = threading.Thread(target=worker, args=(q, inf, read_sz,))
 | 
				
			||||||
 | 
					        thr.daemon = True
 | 
				
			||||||
 | 
					        thr.start()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for fn in get_files(root):
 | 
				
			||||||
 | 
					        q.put(fn)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for _ in range(num_threads):
 | 
				
			||||||
 | 
					        q.put(None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    while inf.n_done < num_threads:
 | 
				
			||||||
 | 
					        time.sleep(0.1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    t2 = time.time()
 | 
				
			||||||
 | 
					    print("\n")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    log = inf.reports
 | 
				
			||||||
 | 
					    log.sort()
 | 
				
			||||||
 | 
					    for nbyte, nsec, fn in log[-64:]:
 | 
				
			||||||
 | 
					        print(f"{get_spd(nbyte, nsec)}   {fn}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    print()
 | 
				
			||||||
 | 
					    print("\n".join(inf.errors))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    print(get_spd(inf.n_byte, t2 - t0))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if __name__ == "__main__":
 | 
				
			||||||
 | 
					    main()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										84
									
								
								srv/ceditable.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								srv/ceditable.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,84 @@
 | 
				
			|||||||
 | 
					<!DOCTYPE html><html><head>
 | 
				
			||||||
 | 
					<meta charset="utf-8">
 | 
				
			||||||
 | 
					<meta http-equiv="X-UA-Compatible" content="IE=edge">
 | 
				
			||||||
 | 
					<meta name="viewport" content="width=device-width, initial-scale=0.8">
 | 
				
			||||||
 | 
					<style>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					* {
 | 
				
			||||||
 | 
					    margin: 0;
 | 
				
			||||||
 | 
					    padding: 0;
 | 
				
			||||||
 | 
					    outline: 0;
 | 
				
			||||||
 | 
					    border: none;
 | 
				
			||||||
 | 
					    font-size: 1em;
 | 
				
			||||||
 | 
					    line-height: 1em;
 | 
				
			||||||
 | 
					    font-family: monospace, monospace;
 | 
				
			||||||
 | 
					    color: #333;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					html, body {
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					    height: 100%;
 | 
				
			||||||
 | 
					    background: #ddd;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					html {
 | 
				
			||||||
 | 
					    font-size: 1.3em;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					li, #edit {
 | 
				
			||||||
 | 
					    list-style-type: none;
 | 
				
			||||||
 | 
					    white-space: pre-wrap;
 | 
				
			||||||
 | 
					    word-break: break-all;
 | 
				
			||||||
 | 
					    overflow-wrap: break-word;
 | 
				
			||||||
 | 
					    word-wrap: break-word; /*ie*/
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					li:nth-child(even) {
 | 
				
			||||||
 | 
					    background: #ddd;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					#edit, #html, #txt1, #txt2 {
 | 
				
			||||||
 | 
					    background: #eee;
 | 
				
			||||||
 | 
					    position: fixed;
 | 
				
			||||||
 | 
					    width: calc(50% - .8em);
 | 
				
			||||||
 | 
					    height: calc(50% - .8em);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					#txt1 { top: .5em; left: .5em }
 | 
				
			||||||
 | 
					#edit { top: .5em; right: .5em }
 | 
				
			||||||
 | 
					#html { bottom: .5em; left: .5em }
 | 
				
			||||||
 | 
					#txt2 { bottom: .5em; right: .5em }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</style></head><body>
 | 
				
			||||||
 | 
					<pre id="edit" contenteditable="true"></pre>
 | 
				
			||||||
 | 
					<textarea id="html"></textarea>
 | 
				
			||||||
 | 
					<ul id="txt1"></ul>
 | 
				
			||||||
 | 
					<ul id="txt2"></ul>
 | 
				
			||||||
 | 
					<script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var edit = document.getElementById('edit'),
 | 
				
			||||||
 | 
					    html = document.getElementById('html'),
 | 
				
			||||||
 | 
					    txt1 = document.getElementById('txt1'),
 | 
				
			||||||
 | 
					    txt2 = document.getElementById('txt2');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var oh = null;
 | 
				
			||||||
 | 
					function fun() {
 | 
				
			||||||
 | 
					    var h = edit.innerHTML;
 | 
				
			||||||
 | 
					    if (oh != h) {
 | 
				
			||||||
 | 
					        oh = h;
 | 
				
			||||||
 | 
					        html.value = h;
 | 
				
			||||||
 | 
					        var t = edit.innerText;
 | 
				
			||||||
 | 
					        if (h.indexOf('<div><br></div>') >= 0)
 | 
				
			||||||
 | 
					            t = t.replace(/\n\n/g, "\n");
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        t = '<li>' + t.
 | 
				
			||||||
 | 
					            replace(/&/g, "&").
 | 
				
			||||||
 | 
					            replace(/</g, "<").
 | 
				
			||||||
 | 
					            replace(/>/g, ">").
 | 
				
			||||||
 | 
					            split('\n').join('</li>\n<li>') + '</li>';
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        t = t.replace(/<li><\/li>/g, '<li> </li>');
 | 
				
			||||||
 | 
					        txt1.innerHTML = t;
 | 
				
			||||||
 | 
					        txt2.innerHTML = t;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    setTimeout(fun, 100);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					fun();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					</body>
 | 
				
			||||||
 | 
					</html>
 | 
				
			||||||
							
								
								
									
										56
									
								
								srv/test.md
									
									
									
									
									
								
							
							
						
						
									
										56
									
								
								srv/test.md
									
									
									
									
									
								
							@@ -1,3 +1,54 @@
 | 
				
			|||||||
 | 
					### hello world
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					* qwe
 | 
				
			||||||
 | 
					* asd
 | 
				
			||||||
 | 
					  * zxc
 | 
				
			||||||
 | 
					  * 573
 | 
				
			||||||
 | 
					    * one
 | 
				
			||||||
 | 
					    * two
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					  * |||
 | 
				
			||||||
 | 
					    |--|--|
 | 
				
			||||||
 | 
					    |listed|table|
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					[72....................................................................]
 | 
				
			||||||
 | 
					[80............................................................................]
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					* foo
 | 
				
			||||||
 | 
					  ```
 | 
				
			||||||
 | 
					  [72....................................................................]
 | 
				
			||||||
 | 
					  [80............................................................................]
 | 
				
			||||||
 | 
					  ```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  * bar
 | 
				
			||||||
 | 
					    ```
 | 
				
			||||||
 | 
					    [72....................................................................]
 | 
				
			||||||
 | 
					    [80............................................................................]
 | 
				
			||||||
 | 
					    ```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					l[i]=1I;(){}o0O</> var foo = "$(`bar`)"; a's'd
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					🔍🌽.📕.🍙🔎
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[](#s1)
 | 
				
			||||||
 | 
					[s1](#s1)
 | 
				
			||||||
 | 
					[#s1](#s1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					a123456789b123456789c123456789d123456789e123456789f123456789g123456789h123456789i123456789j123456789k123456789l123456789m123456789n123456789o123456789p123456789q123456789r123456789s123456789t123456789u123456789v123456789w123456789x123456789y123456789z123456789
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<foo>   bar & <span>baz</span>
 | 
				
			||||||
 | 
					<a href="?foo=bar&baz=qwe&rty">?foo=bar&baz=qwe&rty</a>
 | 
				
			||||||
 | 
					<!-- hidden -->
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					<foo>   bar & <span>baz</span>
 | 
				
			||||||
 | 
					<a href="?foo=bar&baz=qwe&rty">?foo=bar&baz=qwe&rty</a>
 | 
				
			||||||
 | 
					<!-- visible -->
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
*fails marked/showdown/tui/simplemde (just italics), **OK: markdown-it/simplemde:***  
 | 
					*fails marked/showdown/tui/simplemde (just italics), **OK: markdown-it/simplemde:***  
 | 
				
			||||||
testing just google.com and underscored _google.com_ also with _google.com,_ trailing comma and _google.com_, comma after
 | 
					testing just google.com and underscored _google.com_ also with _google.com,_ trailing comma and _google.com_, comma after
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -83,6 +134,11 @@ a newline toplevel
 | 
				
			|||||||
| a table | on the right |
 | 
					| a table | on the right |
 | 
				
			||||||
| second row | foo bar |
 | 
					| second row | foo bar |
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					||
 | 
				
			||||||
 | 
					--|:-:|-:
 | 
				
			||||||
 | 
					a table | big text in this | aaakbfddd
 | 
				
			||||||
 | 
					second row | centred | bbb
 | 
				
			||||||
 | 
					
 | 
				
			||||||
* list entry
 | 
					* list entry
 | 
				
			||||||
* [x] yes
 | 
					* [x] yes
 | 
				
			||||||
* [ ] no
 | 
					* [ ] no
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user