diff --git a/share.config b/share.config new file mode 100644 index 0000000..d680eb4 --- /dev/null +++ b/share.config @@ -0,0 +1,19 @@ +[server] +port = 3001 +dir = ~/Downloads/shared +ttl = 7 +san = +lang = ru +refresh = 5 +per_page = 10,25,50,100 + +[internal] +secs_per_day = 86400 +cleanup_interval = 3600 +ssl_handshake_timeout = 5 +sniff_size = 8192 +chunk_size = 65536 +cert_days = 3650 +cert_key_bits = 2048 +toast_ms = 2500 +progress_hide_ms = 1200 diff --git a/share.py b/share.py index 6f71555..6e6cb97 100755 --- a/share.py +++ b/share.py @@ -24,7 +24,7 @@ def cleanup_loop(upload_dir: Path, max_age: int): if max_age <= 0: return while True: - time.sleep(3600) + time.sleep(CLEANUP_INTERVAL) now = time.time() try: entries = list(upload_dir.iterdir()) @@ -68,9 +68,9 @@ def ensure_cert(cert_dir: Path, extra_ips: list[str] | None = None) -> tuple[Pat san = ",".join([f"IP:{ip}" for ip in sorted(ips)] + ["DNS:localhost"]) subprocess.run([ - "openssl", "req", "-x509", "-newkey", "rsa:2048", + "openssl", "req", "-x509", "-newkey", f"rsa:{CERT_KEY_BITS}", "-keyout", str(key), "-out", str(cert), - "-days", "3650", "-nodes", + "-days", str(CERT_DAYS), "-nodes", "-subj", "/CN=Share", "-addext", f"subjectAltName={san}", ], check=True, capture_output=True) @@ -80,6 +80,32 @@ def ensure_cert(cert_dir: Path, extra_ips: list[str] | None = None) -> tuple[Pat BASE_DIR = Path(__file__).resolve().parent +# ── Config ── + +_cp = configparser.ConfigParser() +_cp.read_dict({ + "server": { + "port": "3001", "dir": str(Path.home() / "Downloads" / "shared"), + "ttl": "7", "san": "", "lang": "ru", "refresh": "5", + "per_page": "10,25,50,100", + }, + "internal": { + "secs_per_day": "86400", "cleanup_interval": "3600", + "ssl_handshake_timeout": "5", "sniff_size": "8192", + "chunk_size": "65536", "cert_days": "3650", "cert_key_bits": "2048", + "toast_ms": "2500", "progress_hide_ms": "1200", + }, +}) +_cp.read(BASE_DIR / "share.config") + +SECS_PER_DAY = _cp.getint("internal", "secs_per_day") +CLEANUP_INTERVAL = _cp.getint("internal", "cleanup_interval") +SSL_HANDSHAKE_TIMEOUT = _cp.getint("internal", "ssl_handshake_timeout") +SNIFF_SIZE = _cp.getint("internal", "sniff_size") +CHUNK_SIZE = _cp.getint("internal", "chunk_size") +CERT_DAYS = _cp.getint("internal", "cert_days") +CERT_KEY_BITS = _cp.getint("internal", "cert_key_bits") + def _file_type(path: Path) -> str: ct = mimetypes.guess_type(path.name)[0] or '' if ct.startswith('image/'): @@ -88,7 +114,7 @@ def _file_type(path: Path) -> str: return 'text' try: with path.open('rb') as f: - return 'text' if b'\x00' not in f.read(8192) else 'other' + return 'text' if b'\x00' not in f.read(SNIFF_SIZE) else 'other' except OSError: return 'other' @@ -106,7 +132,7 @@ class ShareServer(http.server.ThreadingHTTPServer): def get_request(self): client, addr = self.socket.accept() if self.ssl_ctx: - client.settimeout(5) + client.settimeout(SSL_HANDSHAKE_TIMEOUT) try: client = self.ssl_ctx.wrap_socket(client, server_side=True) except (ssl.SSLError, OSError): @@ -195,7 +221,7 @@ class Handler(http.server.BaseHTTPRequestHandler): self.send_header("Connection", "close") self.end_headers() with safe.open("rb") as f: - while chunk := f.read(65536): + while chunk := f.read(CHUNK_SIZE): self.wfile.write(chunk) def _route_upload(self, _prefix): @@ -252,24 +278,9 @@ class Handler(http.server.BaseHTTPRequestHandler): # ── Main ── -DEFAULTS = { - "port": "3001", - "dir": str(Path.home() / "Downloads" / "shared"), - "ttl": "7", - "san": "", - "lang": "ru", - "refresh": "5", - "per_page": "10,25,50,100", -} - - def load_config() -> dict: - """Load config: defaults ← config file ← CLI args.""" - cfg_path = BASE_DIR / "share.conf" - cp = configparser.ConfigParser() - cp.read_dict({"share": DEFAULTS}) - cp.read(cfg_path) - conf = dict(cp["share"]) + """Load config: share.config defaults ← CLI args.""" + conf = dict(_cp["server"]) p = argparse.ArgumentParser(description="Share — local file sharing server") p.add_argument("-p", "--port", type=int) @@ -292,7 +303,7 @@ def main(): port = int(conf["port"]) upload_dir = Path(conf["dir"]).expanduser().resolve() - max_age = int(conf["ttl"]) * 86400 + max_age = int(conf["ttl"]) * SECS_PER_DAY san_list = conf["san"].split() if conf["san"] else None upload_dir.mkdir(parents=True, exist_ok=True) @@ -304,6 +315,8 @@ def main(): "lang": conf["lang"], "refresh": int(conf["refresh"]), "perPage": [int(x) for x in conf["per_page"].split(",")], + "toastMs": _cp.getint("internal", "toast_ms"), + "progressHideMs": _cp.getint("internal", "progress_hide_ms"), }) cfg_tag = f"".encode() html = (BASE_DIR / "static" / "index.html").read_bytes() diff --git a/static/index.html b/static/index.html index 208b37d..b227b21 100644 --- a/static/index.html +++ b/static/index.html @@ -194,6 +194,8 @@ const _cfg = typeof CFG !== 'undefined' ? CFG : {}; const defaultLang = _cfg.lang || 'ru'; const REFRESH_MS = (_cfg.refresh || 5) * 1000; const PER_PAGE_OPTS = _cfg.perPage || [10, 25, 50, 100]; +const TOAST_MS = _cfg.toastMs || 2500; +const PROGRESS_HIDE_MS = _cfg.progressHideMs || 1200; /* ── i18n ── */ @@ -266,7 +268,7 @@ function toast(msg) { toastEl.textContent = msg; toastEl.classList.add('show'); clearTimeout(toastTimer); - toastTimer = setTimeout(() => toastEl.classList.remove('show'), 2500); + toastTimer = setTimeout(() => toastEl.classList.remove('show'), TOAST_MS); } function fmtSize(b) { @@ -371,7 +373,7 @@ function upload(fileList) { xhr.onload = () => { done(); pfill.style.width = '100%'; - setTimeout(() => { $('progress').style.display = 'none'; pfill.style.width = '0' }, 1200); + setTimeout(() => { $('progress').style.display = 'none'; pfill.style.width = '0' }, PROGRESS_HIDE_MS); if (xhr.status === 200) { const r = JSON.parse(xhr.responseText); toast(t().uploaded(r.count));