#!/usr/bin/python3 import sys import signal import os import argparse import functools from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler, HTTPStatus from http import cookies from urllib import parse import html import mimetypes import base64 import hashlib import io import time import stat import re import socket import ssl import tarfile import fnmatch class ShareServer(ThreadingHTTPServer): pass class ZstdAdapter: def compress(self, data): raise NotImplementedError def get_writer(self, file): raise NotImplementedError class InternalZstdAdapter(ZstdAdapter): def __init__(self): from compression import zstd self._zstd = zstd self._options = {zstd.CompressionParameter.checksum_flag: True} def compress(self, data): return self._zstd.compress(data, options=self._options) def get_writer(self, file): return self._zstd.open(file, 'wb', options=self._options) class ExnternalZstdAdapter(ZstdAdapter): def __init__(self): import zstandard self._zstd = zstandard.ZstdCompressor(write_checksum=True) def compress(self, data): return self._zstd.compress(data) def get_writer(self, file): return self._zstd.stream_writer(file, write_return_read=True, closefd=False) class BaseHandler(BaseHTTPRequestHandler): protocol_version = 'HTTP/1.1' ico = base64.b64decode( 'AAABAAMAMDAAAAEAIACoJQAANgAAACAgAAABACAAqBAAAN4lAAAQEAAAAQAgAGgEAACGNgAAKAAAADAAAABgAAAAAQAgAAAAAAAAJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHGzPAFxszwhcbM8U3GzPGZxszxncbM8Z3GzPGdxszxncbM8Z3GzPGdxszxncbM8Z3GzPGdxszxncbM8Z3GzPGdxszxncbM8Z3GzPGdxszxncbM8Z3GzPGdxszxncbM8Z3GzPGdxszxncbM8Z3GzPGdxszxncbM8Z3GzPGdxszxncbM8Z3GzPGdxszxncbM8Z3GzPGdxszxncbM8ZnGzPFNxszwhcbM8AQAAAAAAAAAAAAAAAAAAAAAAAAAAcbM8B3GzPGxxszzccbM8/HGzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPPxxszzccbM8bHGzPAcAAAAAAAAAAAAAAAAAAAAAcbM8bHGzPPZxszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM89nGzPGwAAAAAAAAAAAAAAABxszwhcbM823GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPNtxszwhAAAAAAAAAABxszxTcbM8+3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPPtxszxTAAAAAAAAAABxszxmcbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszxmAAAAAAAAAABxszxncbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszxnAAAAAAAAAABxszxncbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8+3GzPOFxszzHcbM8x3GzPOFxszz8cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszxnAAAAAAAAAABxszxncbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszzccbM8bHGzPCJxszwMcbM8DXGzPCJxszxscbM83XGzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszxnAAAAAAAAAABxszxncbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPNNxszw3AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcbM8OHGzPNRxszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszxnAAAAAAAAAABxszxncbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM88XGzPE0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHGzPE5xszzycbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszxnAAAAAAAAAABxszxncbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8s3GzPAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHGzPAlxszy0cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszxnAAAAAAAAAABxszxncbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8fQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABxszx+cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszxnAAAAAAAAAABxszxncbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8bgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABxszxvcbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszxnAAAAAAAAAABxszxncbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz+cbM8cwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABxszyFcbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszxnAAAAAAAAAABxszxncbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPOFxszx4cbM8FQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHGzPBBxszzCcbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszxnAAAAAAAAAABxszxncbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz0cbM8onGzPC9xszwBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHGzPGZxszz5cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszxnAAAAAAAAAABxszxncbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP5xszzccbM8mXGzPHFxszxxcbM8mHGzPNxxszz+cbM8/XGzPMdxszxScbM8CAAAAAAAAAAAcbM8AXGzPDJxszxDcbM8BAAAAAAAAAAAAAAAAAAAAABxszwDcbM8V3GzPOZxszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszxnAAAAAAAAAABxszxncbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8+3GzPKBxszwkcbM8AQAAAAAAAAAAcbM8AXGzPCVxszyTcbM8fHGzPBcAAAAAAAAAAAAAAABxszwXcbM8enGzPORxszzvcbM8lXGzPEJxszwkcbM8JHGzPENxszyVcbM873GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszxnAAAAAAAAAABxszxncbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8oHGzPA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABxszwEcbM8AgAAAAAAAAAAcbM8B3GzPFFxszzGcbM8/XGzPP9xszz/cbM8/3GzPPVxszzlcbM85XGzPPVxszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszxnAAAAAAAAAABxszxncbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszzdcbM8JwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHGzPAFxszwucbM8oXGzPPRxszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszxnAAAAAAAAAABxszxncbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszyacbM8AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHGzPFRxszzicbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszxnAAAAAAAAAABxszxncbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszxzAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHGzPHRxszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszxnAAAAAAAAAABxszxncbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszxzAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHGzPHRxszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszxnAAAAAAAAAABxszxncbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszyacbM8AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHGzPFNxszzicbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszxnAAAAAAAAAABxszxncbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszzdcbM8JwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHGzPAFxszwucbM8oHGzPPNxszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszxnAAAAAAAAAABxszxncbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8n3GzPA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABxszwEcbM8AgAAAAAAAAAAcbM8B3GzPFFxszzGcbM8/XGzPP9xszz/cbM8/3GzPPVxszzlcbM85XGzPPVxszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszxnAAAAAAAAAABxszxncbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8+3GzPKBxszwkcbM8AQAAAAAAAAAAcbM8AXGzPCVxszyTcbM8fHGzPBcAAAAAAAAAAAAAAABxszwWcbM8enGzPORxszzvcbM8lXGzPEJxszwkcbM8JHGzPENxszyVcbM873GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszxnAAAAAAAAAABxszxncbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP5xszzccbM8mXGzPHFxszxxcbM8mHGzPNxxszz+cbM8/XGzPMdxszxScbM8CAAAAAAAAAAAcbM8AXGzPDJxszxCcbM8BAAAAAAAAAAAAAAAAAAAAABxszwDcbM8VnGzPOZxszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszxnAAAAAAAAAABxszxncbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz0cbM8onGzPC9xszwBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHGzPGZxszz5cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszxnAAAAAAAAAABxszxncbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPOJxszx4cbM8FQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHGzPBBxszzCcbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszxnAAAAAAAAAABxszxncbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz+cbM8cwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABxszyFcbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszxnAAAAAAAAAABxszxncbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8bgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABxszxvcbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszxnAAAAAAAAAABxszxncbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8fQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABxszx+cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszxnAAAAAAAAAABxszxncbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8s3GzPAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHGzPAlxszy0cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszxnAAAAAAAAAABxszxncbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM88XGzPE0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHGzPE5xszzycbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszxnAAAAAAAAAABxszxncbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPNNxszw3AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcbM8OHGzPNRxszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszxnAAAAAAAAAABxszxncbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszzccbM8bHGzPCJxszwMcbM8DXGzPCJxszxscbM83XGzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszxnAAAAAAAAAABxszxncbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8+3GzPOFxszzHcbM8x3GzPOFxszz8cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszxnAAAAAAAAAABxszxncbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszxnAAAAAAAAAABxszxmcbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszxmAAAAAAAAAABxszxTcbM8+3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPPtxszxTAAAAAAAAAABxszwhcbM823GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPNtxszwhAAAAAAAAAAAAAAAAcbM8bHGzPPZxszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM89nGzPGwAAAAAAAAAAAAAAAAAAAAAcbM8B3GzPGxxszzccbM8/HGzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPPxxszzccbM8bHGzPAcAAAAAAAAAAAAAAAAAAAAAAAAAAHGzPAFxszwhcbM8U3GzPGZxszxncbM8Z3GzPGdxszxncbM8Z3GzPGdxszxncbM8Z3GzPGdxszxncbM8Z3GzPGdxszxncbM8Z3GzPGdxszxncbM8Z3GzPGdxszxncbM8Z3GzPGdxszxncbM8Z3GzPGdxszxncbM8Z3GzPGdxszxncbM8Z3GzPGdxszxncbM8Z3GzPGdxszxncbM8ZnGzPFNxszwhcbM8AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///////wAA////////AADwAAAAAA8AAOAAAAAABwAAwAAAAAADAADAAAAAAAMAAMAAAAAAAwAAwAAAAAADAADAAAAAAAMAAMAAAAfgAwAAwAAAD/ADAADAAAAf+AMAAMAAAB/4AwAAwAAAP/wDAADAAAA//AMAAMAAAD/4AwAAwAAAf/gDAADAAAD/+AMAAMABg//wAwAAwAfv48ADAADAD/+AAAMAAMAf/gAAAwAAwB/8AAADAADAP/wAAAMAAMA//AAAAwAAwB/8AAADAADAH/4AAAMAAMAP/4AAAwAAwAfv48ADAADAAYP/8AMAAMAAAP/4AwAAwAAAf/gDAADAAAA/+AMAAMAAAD/8AwAAwAAAP/wDAADAAAAf+AMAAMAAAB/4AwAAwAAAD/ADAADAAAAH4AMAAMAAAAAAAwAAwAAAAAADAADAAAAAAAMAAMAAAAAAAwAAwAAAAAADAADgAAAAAAcAAPAAAAAADwAA////////AAD///////8AACgAAAAgAAAAQAAAAAEAIAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHGzPAFxszwOcbM8GXGzPBlxszwZcbM8GXGzPBlxszwZcbM8GXGzPBlxszwZcbM8GXGzPBlxszwZcbM8GXGzPBlxszwZcbM8GXGzPBlxszwZcbM8GXGzPBlxszwZcbM8GXGzPBlxszwZcbM8DnGzPAEAAAAAAAAAAAAAAABxszwHcbM8ZHGzPMFxszzUcbM81HGzPNRxszzUcbM81HGzPNRxszzUcbM81HGzPNRxszzUcbM81HGzPNRxszzUcbM81HGzPNRxszzUcbM81HGzPNRxszzUcbM81HGzPNRxszzUcbM81HGzPNRxszzBcbM8ZHGzPAcAAAAAAAAAAHGzPGRxszz2cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz2cbM8ZAAAAABxszwNcbM8v3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszy/cbM8DXGzPBhxszzUcbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPNRxszwYcbM8GXGzPNRxszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPPlxszzdcbM80HGzPOdxszz+cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM81HGzPBlxszwZcbM81HGzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszzjcbM8Z3GzPBxxszwRcbM8K3GzPJRxszz4cbM8/3GzPP9xszz/cbM8/3GzPP9xszzUcbM8GXGzPBlxszzUcbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM883GzPFcAAAAAAAAAAAAAAAAAAAAAcbM8CXGzPJhxszz/cbM8/3GzPP9xszz/cbM8/3GzPNRxszwZcbM8GXGzPNRxszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3Gz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PP9xszz/cbM8/3GzPP9xszz/cbM8nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHGzPBpxszzbcbM8/3GzPP9xszz/cbM8/3GzPNRxszwZcbM8GXGzPNRxszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszy8cbM8CwAAAAAAAAAAAAAAAAAAAAAAAAAAcbM8NHGzPO1xszz/cbM8/3GzPP9xszz/cbM81HGzPBlxszwZcbM81HGzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPPNxszxXAAAAAAAAAAAAAAAAAAAAAHGzPAlxszyYcbM8/3GzPP9xszz/cbM8/3GzPP9xszzUcbM8GXGzPBlxszzUcbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPONxszxncbM8HHGzPBFxszwrcbM8lHGzPPhxszz/cbM8/3GzPP9xszz/cbM8/3GzPNRxszwZcbM8GXGzPNRxszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPPlxszzdcbM80HGzPOdxszz+cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM81HGzPBlxszwYcbM81HGzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszzUcbM8GHGzPA1xszy/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPL9xszwNAAAAAHGzPGRxszz2cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3GzPP9xszz/cbM8/3Gz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' ) start_time = time.gmtime() # fmt: off _control_char_table = str.maketrans({c: fr'\x{c:02x}' for c in (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 92, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159)}) # fmt: on def __init__(self, *args): self._validated = False self._hostname = socket.gethostname() if sys.version_info >= (3, 14): self._zstd = InternalZstdAdapter() else: try: self._zstd = ExnternalZstdAdapter() except Exception: self._zstd = None self._should_log_time = True if os.getenv('SHARE_LOG_TIME', 'true') == 'true' else False super().__init__(*args) def handle_one_request(self): try: super().handle_one_request() except Exception as e: self.close_connection = True self.log_error(f'{type(e).__name__}: {e}'.removesuffix(': ')) def can_access(self, path): if not self.password or self._validated: return True if path.endswith('.tar.zst'): path = path.removesuffix('.tar.zst').rstrip('/') + '/' if bool(self.auth_pattern.match(path)) ^ self.invert_match: return False if bool(self.auth_pattern.match(path)) ^ self.invert_match: return False return True def do_GET(self): self._validate_password() self._split_path() if self._path_only == '/favicon.ico': self.respond_with_file('favicon.ico') return if 'login' in self._queries: self.respond_with_html(self._build_html_for_password()) return if self.can_access(self._path_only): self.handle_get() return if self.get_accept_content_type() == 'text/plain': self.respond_unauthorized() else: self.respond_redirect(f'{parse.quote(self._path_only)}?login', connection='close') def do_POST(self): self._validate_password() self._split_path() if 'login' in self._queries: content_length = self.get_content_length() if not content_length or content_length > 100: self.respond_bad_request() return data = self.rfile.read(content_length).decode() data = parse.unquote_plus(data) password, _, remember_device = data.partition('&') if password == f'password={self.password}': cookie = f'password={parse.quote_plus(self.encoded_password)}; path=/' if remember_device == 'remember_device=on': cookie += '; max-age=2592000' cookie += '; HttpOnly' redirect_url = parse.quote(self._path_only) connection = None else: cookie = None redirect_url = f'{parse.quote(self._path_only)}?login' connection = 'close' self.respond_redirect(redirect_url, cookie=cookie, connection=connection) return if self.can_access(self._path_only): self.handle_post() return self.respond_unauthorized() def do_PUT(self): self._validate_password() self._split_path() if self.can_access(self._path_only): self.handle_put() return self.respond_unauthorized() def handle_get(self): self.respond_method_not_allowed() def handle_post(self): self.respond_method_not_allowed() def handle_put(self): self.respond_method_not_allowed() def get_content_length(self): content_length = self.headers['Content-Length'] if not content_length or not content_length.isdecimal(): return None return int(content_length) def get_accept_content_type(self): act = self.headers.get('Accept', '').split(',')[0] act, _, q = act.partition(';') if act == '*/*' and self.headers.get('User-Agent', '').startswith('curl'): act = 'text/plain' return act def get_accept_encodings(self): accept_encoding = self.headers['Accept-Encoding'] if not accept_encoding: return set() return set(accept_encoding.split(', ')) def get_if_modified_since(self): try: if_modified_since = time.strptime( self.headers['If-Modified-Since'], '%a, %d %b %Y %H:%M:%S GMT' ) except Exception: return None else: return time.struct_time((*if_modified_since[:-1], 0)) def handle_multipart(self, save_dir, path): if not path.endswith('/'): self.respond_bad_request() return save_dir = f'{save_dir.rstrip("/\\")}{path}' content_length = self.get_content_length() if not content_length: self.respond_bad_request() return content_type = self.headers['Content-Type'] if not content_type: self.respond_bad_request() return boundary = self._parse_boundary(content_type) if not boundary: self.respond_bad_request() return try: os.makedirs(save_dir, exist_ok=True) save_dir = save_dir.rstrip('/\\') for mf in MultipartParser(self.rfile, boundary, content_length): if mf.name != 'file': self.respond_bad_request() return with open(f'{save_dir}/{mf.filename}', 'wb') as f: mf.transfer_to(f) except MultipartError: self.respond_bad_request() except PermissionError: self.respond_forbidden() except (FileExistsError, IsADirectoryError): self.respond_internal_server_error() else: if self.get_accept_content_type() == 'text/plain': self.respond(HTTPStatus.OK, content_length='0') else: self.respond_redirect(parse.quote(path)) def handle_putfile(self, file_path): if file_path.endswith('/'): self.respond_bad_request() return content_length = self.get_content_length() if not content_length: self.respond_bad_request() return try: os.makedirs(os.path.dirname(file_path), exist_ok=True) with open(file_path, 'wb') as f: while content_length: l = min(content_length, 65536) f.write(self.rfile.read(l)) content_length -= l except PermissionError: self.respond_forbidden() except (FileExistsError, IsADirectoryError): self.respond_internal_server_error() else: self.respond(HTTPStatus.OK, content_length='0') def send_response(self, code, message=None): self.log_request(code) self.send_response_only(code, message) def send_error(self, code, message=None, explain=None): command = self.command if self.command else '???' if message is None: if code in self.responses: message = self.responses[code][0] else: message = '' try: detail = parse.unquote(self.path) except AttributeError: detail = message self.log_error(f'{command} {code} {detail}') self.send_response_only(code, message) self.send_header('Connection', 'close') if code == HTTPStatus.UNAUTHORIZED: self.send_header('WWW-Authenticate', 'Basic realm="Realm"') self.end_headers() def _validate_password(self): if not self.password: return self._validated = False while not self._validated: authorization = self.headers['Authorization'] if not authorization: break scheme, _, credential = authorization.partition(' ') if scheme != 'Basic' or not credential: break credential = base64.b64decode(credential).decode() if not credential: break username, _, password = credential.partition(':') if username != 'user' or password != self.password: break self._validated = True while not self._validated: cookie = self.headers['Cookie'] if not cookie: break password = cookies.SimpleCookie(cookie).get('password') if not password: break if parse.unquote_plus(password.value) != self.encoded_password: break self._validated = True def _build_html_for_password(self): builder = HtmlBuilder() builder.start_head() builder.start_title() builder.append(self._hostname) builder.end_title() builder.start_style() builder.append( '.container{height: 80%; display: flex; align-items: center; justify-content: center;}' ) builder.append('.password{margin-top: 4px; margin-bottom: 12px; line-height: 24px;}') builder.append('.submit{width: 100%; height: 32px; margin-top: 24px;}') builder.end_style() builder.end_head() builder.start_body() builder.append('
') builder.append(f'
') builder.append('') builder.append( '' ) builder.append('
') builder.append('') builder.append('') builder.append('
') builder.append('') builder.append('
') builder.append('
') builder.end_body() return builder.build() def _parse_boundary(self, content_type): parts = content_type.split('; ') if len(parts) != 2: return None form_data, boundary = parts if form_data != 'multipart/form-data': return None parts = boundary.split('=') if len(parts) != 2: return None key, value = parts if key != 'boundary': return None return value def _parse_range(self, content_range, filesize): if len(content_range) < 8 or content_range[:6] != 'bytes=': return None parts = content_range[6:].split('-') if len(parts) != 2: return None start, end = parts if not start.isdecimal(): return None if end and not end.isdecimal(): return None start = int(start) end = int(end) if end else filesize - 1 if start > end or end >= filesize: return None return (start, end) def _split_path(self): path, _, query = parse.unquote(self.path).partition('?') parts = [] for p in path.split('/'): if p == '..': if parts: parts.pop() elif p and p != '.': parts.append(p) collapsed_path = '/' + '/'.join(parts) if path != '/' and path.endswith('/'): collapsed_path += '/' queries = {} for item in query.split('&'): key, _, value = item.partition('=') queries[key] = value self._path_only, self._queries = collapsed_path, queries def _guess_type(self, path): guess, _ = mimetypes.guess_type(path) if not guess: guess = 'application/octet-stream' if guess.startswith('text'): guess = f'{guess}; charset=utf-8' return guess def respond( self, status, *, content_type=None, content_length=None, last_modified=None, transfer_encoding=None, content_encoding=None, accept_ranges=None, content_range=None, content_disposition=None, location=None, cookie=None, connection=None, ): self.send_response(status) if content_type is not None: self.send_header('Content-Type', content_type) if content_length is not None: self.send_header('Content-Length', content_length) if last_modified is not None: self.send_header('Cache-Control', 'public, no-cache') self.send_header( 'Last-Modified', time.strftime('%a, %d %b %Y %H:%M:%S GMT', last_modified) ) if transfer_encoding is not None: self.send_header('Transfer-Encoding', transfer_encoding) if content_encoding is not None: self.send_header('Content-Encoding', content_encoding) if accept_ranges is not None: self.send_header('Accept-Ranges', accept_ranges) if content_range is not None: self.send_header('Content-Range', content_range) if content_disposition is not None: self.send_header('Content-Disposition', content_disposition) if location is not None: self.send_header('Location', location) if cookie is not None: self.send_header('Set-Cookie', cookie) if connection is not None: self.send_header('Connection', connection) self.end_headers() def respond_redirect(self, location, cookie=None, connection=None): self.respond( HTTPStatus.SEE_OTHER, content_length='0', location=location, cookie=cookie, connection=connection, ) def respond_not_modified( self, last_modified, content_type='text/html; charset=utf-8', accept_ranges=None ): self.respond( HTTPStatus.NOT_MODIFIED, content_type=content_type, last_modified=last_modified, accept_ranges=accept_ranges, ) def respond_range_not_satisfiable(self): self.send_error(HTTPStatus.REQUESTED_RANGE_NOT_SATISFIABLE) def respond_bad_request(self): self.send_error(HTTPStatus.BAD_REQUEST) def respond_unauthorized(self): self.send_error(HTTPStatus.UNAUTHORIZED) def respond_forbidden(self): self.send_error(HTTPStatus.FORBIDDEN) def respond_not_found(self): self.send_error(HTTPStatus.NOT_FOUND) def respond_method_not_allowed(self): self.send_error(HTTPStatus.METHOD_NOT_ALLOWED) def respond_internal_server_error(self): self.send_error(HTTPStatus.INTERNAL_SERVER_ERROR) def respond_with_data(self, data, content_type, last_modified=None): if len(data) >= 1024 and 'zstd' in self.get_accept_encodings() and self._zstd: data = self._zstd.compress(data) content_length = len(data) content_encoding = 'zstd' else: content_length = len(data) content_encoding = None self.respond( HTTPStatus.OK, content_type=content_type, content_length=content_length, last_modified=last_modified, content_encoding=content_encoding, ) self.wfile.write(data) def respond_with_html(self, html, last_modified=None): self.respond_with_data(html.encode(), 'text/html; charset=utf-8', last_modified) def respond_with_text(self, text, last_modified=None): self.respond_with_data(text.encode(), 'text/plain; charset=utf-8', last_modified) def respond_with_file(self, file, send_content_disposition=False): if file == 'favicon.ico': filename = 'favicon.ico' filesize = len(self.ico) content_type = 'image/x-icon' else: filename = os.path.basename(file) filesize = os.path.getsize(file) content_type = self._guess_type(file) request_range = self.headers['Range'] if request_range: request_range = self._parse_range(request_range, filesize) if not request_range: self.respond_range_not_satisfiable() return start, end = request_range content_length = end - start + 1 status = HTTPStatus.PARTIAL_CONTENT content_range = f'bytes {start}-{end}/{filesize}' else: start = 0 content_length = filesize status = HTTPStatus.OK content_range = None if file == 'favicon.ico': f = io.BytesIO(self.ico) last_modified = self.start_time else: try: f = open(file, 'rb') last_modified = time.gmtime(os.fstat(f.fileno()).st_mtime) except PermissionError: self.respond_forbidden() return except FileNotFoundError: self.respond_not_found() return accept_ranges = 'bytes' with f: if status == HTTPStatus.OK and self.get_if_modified_since() == last_modified: self.respond_not_modified( last_modified, content_type=content_type, accept_ranges=accept_ranges ) return if ( status == HTTPStatus.OK and content_length >= 1024 and 'zstd' in self.get_accept_encodings() and self._zstd ): compress = True content_length = None transfer_encoding = 'chunked' content_encoding = 'zstd' accept_ranges = None else: compress = False transfer_encoding = None content_encoding = None if send_content_disposition: content_disposition = f'attachment; filename="{parse.quote(filename)}"' else: content_disposition = None self.respond( status, content_type=content_type, content_length=content_length, last_modified=last_modified, transfer_encoding=transfer_encoding, content_encoding=content_encoding, accept_ranges=accept_ranges, content_range=content_range, content_disposition=content_disposition, ) if start: f.seek(start) if compress: with ChunkWriter(self.wfile) as writer: with self._zstd.get_writer(writer) as w: self.copy_stream(f, w, filesize) else: self.copy_stream(f, self.wfile, content_length) def copy_stream(self, reader, writer, size): while size: l = min(size, 65536) writer.write(reader.read(l)) size -= l def log_request(self, code, size=None): self.log_message('%s %d %s', self.command, code, parse.unquote(self.path)) def log_message(self, format, *args): message = (format % args).translate(self._control_char_table) if self._should_log_time: t = time.strftime('%Y/%m/%d %H:%M:%S - ', time.localtime()) else: t = '' sys.stderr.write(f'{t}{self.client_address[0]}:{self.client_address[1]} - {message}\n') class BaseFileShareHandler(BaseHandler): def __init__(self, *args, upload=False, **kwargs): self._upload = upload if is_windows(): self.is_hidden = self._is_hidden_windows else: self.is_hidden = self._is_hidden_unix super().__init__(*args, **kwargs) def respond_with_archive(self, dir_path, send_content_disposition=False): if not self._zstd: self.respond_not_found() return if send_content_disposition: if dir_path == '/': filename = 'root.tar.zst' else: filename = f'{os.path.basename(dir_path.rstrip("/\\"))}.tar.zst' content_disposition = f'attachment; filename="{parse.quote(filename)}"' else: content_disposition = None self.respond( HTTPStatus.OK, content_type='application/zstd', transfer_encoding='chunked', content_disposition=content_disposition, ) with ChunkWriter(self.wfile) as writer: with self._zstd.get_writer(writer) as w: with tarfile.open(None, 'w|', w, 65536) as tar: url_path = self._path_only.removesuffix('.tar.zst').rstrip('/') self.archive_folder(dir_path, url_path, '', tar) def archive_folder(self, dir_path, url_path, arcname, tar): with os.scandir(dir_path) as it: for entry in it: try: if entry.is_dir(): url_name = f'{entry.name}/' else: url_name = entry.name if not self.file_filter(entry.path): continue if not self.can_access(f'{url_path}/{url_name}'): continue arcname = f'{arcname}/{entry.name}' tarinfo = tar.gettarinfo(entry.path, arcname) if not tarinfo: continue if tarinfo.isdir(): tar.addfile(tarinfo) self.archive_folder(entry.path, f'{url_path}/{entry.name}', arcname, tar) elif tarinfo.isfile(): with open(entry.path, 'rb') as f: tar.addfile(tarinfo, f) else: tar.addfile(tarinfo) except (PermissionError, FileNotFoundError): pass def file_filter(self, file_path): raise NotImplementedError def build_text(self, dirs, files): lst = [] for d in dirs: lst.append(f'{d.name}/') for f in files: lst.append(f'{f.name}\t{self._format_size(f.size)}') lst.append('') return '\n'.join(lst) def build_html(self, dirs, files): if self._path_only == '/': title = self._hostname else: title = os.path.basename(self._path_only.rstrip('/')) builder = HtmlBuilder() builder.start_head() builder.start_title() builder.append(title) builder.end_title() builder.start_style() builder.append( '.container{height: 100%; display: flex; flex-direction: column; padding: 0 8px; overflow-wrap: break-word;}' ) builder.append( '.header{display: flex; justify-content: space-between; padding: 14px 0; font-size: xx-large;}' ) builder.append('hr{width: 100%;}') builder.append('.main{flex: auto; padding: 16px 0;}') builder.append('.content{width: 100%; height: 100%;}') builder.append( '.list-item{display: flex; justify-content: space-between; padding: 2px 0; word-break: break-all;}' ) builder.append('@media (prefers-color-scheme: light) {') builder.append('.list-item:nth-child(even){background-color: #f8f8f8;}') builder.append('}') builder.append('@media (prefers-color-scheme: dark) {') builder.append('.list-item:nth-child(even){background-color: #151515;}') builder.append('}') builder.append('.item-left{display: flex;}') builder.append( '.item-right{min-width: 140px; max-width: 140px; display: flex; justify-content: flex-end;}' ) builder.append('.item-icon{flex: none; margin-right: 4px;}') builder.append('.size{font-size: smaller; color: #666666;}') builder.append('iframe{border: 0;}') builder.append('a{color: #0b57d0; text-decoration: none;}') builder.append('a.hidden{color: #128bff;}') builder.append('a:hover{text-decoration: underline;}') builder.append('.btn-download{display: block; height: 20px; margin-left: 4px;}') builder.append('.btn-download:hover{background-color: #e6e6e6; border-radius: 50%;}') if self._upload: builder.append( '.upload{cursor: pointer; background-color: #76797b; border: 1px solid #76797b; color: white; border-radius: 16px;}' ) builder.append('.upload:hover{background-color: #565e64; border-color: #565e64;}') builder.append( '.upload:disabled{opacity: .65; pointer-events: none; user-select: none;}' ) builder.append('@media (prefers-color-scheme: light) {') builder.append('.dragging{border: 4px dashed #cccccc; border-radius: 4px;}') builder.append('}') builder.append('@media (prefers-color-scheme: dark) {') builder.append('.dragging{border: 4px dashed #333333; border-radius: 4px;}') builder.append('}') builder.end_style() if self._upload: builder.start_script() builder.append( ''' function on_upload_click() { document.getElementById("file").click(); } function on_upload() { document.getElementById("upload").setAttribute("disabled", ""); document.getElementById("form").submit(); } let drag_counter = 0; function on_dragenter(e) { e.preventDefault(); drag_counter++; if (drag_counter === 1) { let types = e.dataTransfer.types; if (types[types.length - 1] === "Files") { e.currentTarget.classList.add("dragging"); } } } function on_dragover(e) { e.preventDefault(); } function on_dragleave(e) { e.preventDefault(); drag_counter--; if (drag_counter === 0) { e.currentTarget.classList.remove("dragging"); } } function on_drop(e) { e.preventDefault(); drag_counter = 0; e.currentTarget.classList.remove("dragging"); if (e.dataTransfer.files.length > 0) { document.getElementById("file").files = e.dataTransfer.files; on_upload(); } } function on_load() { let upload = document.getElementById("upload"); upload.onclick = on_upload_click; let content = document.getElementById("content"); content.ondragenter = on_dragenter; content.ondragover = on_dragover; content.ondragleave = on_dragleave; content.ondrop = on_drop; let file = document.getElementById("file"); file.onchange = on_upload; } window.onload = on_load; ''' ) builder.end_script() builder.end_head() builder.start_body() builder.append('
') builder.append('
') builder.append('
') builder.append(f'{html.escape(self._hostname)}') p = '' for name in self._path_only.split('/'): if name: p = f'{p}/{name}' builder.append(f' / {html.escape(name)}') builder.append('
') if self._upload: builder.append('') builder.append( f'') builder.append('
') builder.append('
') builder.append('
') builder.append('
') builder.append('') builder.append('
') builder.append('
') builder.append('
') builder.end_body() return builder.build() def _is_hidden_windows(self, file_path): try: return ( self._is_hidden_unix(file_path) or os.stat(file_path).st_file_attributes & stat.FILE_ATTRIBUTE_HIDDEN != 0 ) except FileNotFoundError: return False def _is_hidden_unix(self, file_path): return os.path.basename(file_path).startswith('.') def _format_size(self, size): if size < 0: return 'unknown' lst = ((1024, 'KiB'), (1048576, 'MiB'), (1073741824, 'GiB'), (1099511627776, 'TiB')) idx = 0 if size < 1048576: idx = 0 elif size < 1073741824: idx = 1 elif size < 1099511627776: idx = 2 else: idx = 3 return f'{size/lst[idx][0]:.2f} {lst[idx][1]}' class VirtualTarShareHandler(BaseFileShareHandler): def __init__(self, dir_path, all_files, *args, **kwargs): self._dir = dir_path.rstrip('/\\') + '/' if dir_path == '/': self._filename = 'root.tar.zst' else: self._filename = f'{os.path.basename(self._dir.rstrip("/"))}.tar.zst' self._all = all_files super().__init__(*args, **kwargs) def handle_get(self): if self._path_only == '/': last_modified = self.start_time if self.get_if_modified_since() == last_modified: self.respond_not_modified(last_modified) return dirs, files = [], [FileItem(self._filename, False, -1)] if self.get_accept_content_type() == 'text/plain': self.respond_with_text(self.build_text(dirs, files), last_modified) else: self.respond_with_html(self.build_html(dirs, files), last_modified) return name = self._path_only[1:] if name == self._filename or name == 'file': self.respond_with_archive(self._dir, name == 'file') return self.respond_not_found() def file_filter(self, file_path): return self._all or not self.is_hidden(file_path) class FileShareHandler(BaseFileShareHandler): def __init__(self, files, *args, **kwargs): self._files = list(files) super().__init__(*args, **kwargs) def handle_get(self): if self._path_only == '/': dirs, files = self.list_files() if self.get_accept_content_type() == 'text/plain': self.respond_with_text(self.build_text(dirs, files)) else: self.respond_with_html(self.build_html(dirs, files)) return name = self._path_only[1:] file_path = self._find_file(name) if file_path: self.respond_with_file(file_path) return if len(self._files) == 1 and name == 'file': self.respond_with_file(self._files[0], True) return self.respond_not_found() def list_files(self): files = [] for f in self._files: try: files.append(FileItem(os.path.basename(f), self.is_hidden(f), os.path.getsize(f))) except Exception: pass return ([], sorted(files)) def _find_file(self, name): for f in self._files: if os.path.basename(f) == name: return f return None class DirectoryShareHandler(BaseFileShareHandler): def __init__(self, dir_path, all_files, *args, **kwargs): self._dir = dir_path.rstrip('/\\') + '/' self._all = all_files if is_windows(): self._contains_hidden_segment = self._contains_hidden_segment_windows else: self._contains_hidden_segment = self._contains_hidden_segment_unix super().__init__(*args, **kwargs) def handle_get(self): if not self.is_url_valid(self._path_only): self.respond_not_found() return full_path = self._dir.rstrip('/') + self._path_only if os.path.isdir(full_path): if not self._path_only.endswith('/'): self.respond_redirect(parse.quote(f'{self._path_only}/')) return try: dirs, files = self.list_dir(full_path) except PermissionError: self.respond_forbidden() return except FileNotFoundError: self.respond_not_found() return if self.get_accept_content_type() == 'text/plain': self.respond_with_text(self.build_text(dirs, files)) else: self.respond_with_html(self.build_html(dirs, files)) return if os.path.isfile(full_path): self.respond_with_file(full_path) return if full_path.endswith('.tar.zst'): full_path = full_path[:-8] if os.path.isdir(full_path): self.respond_with_archive(full_path) return self.respond_not_found() def is_url_valid(self, path): return self._all or not self._contains_hidden_segment(path) def list_dir(self, dir_path): if not dir_path.endswith('/'): dir_path = dir_path + '/' dirs, files = [], [] with os.scandir(dir_path) as it: for entry in it: try: if self.file_filter(entry.path): if entry.is_dir(): dirs.append(FileItem(entry.name, self.is_hidden(entry.path), None)) else: files.append( FileItem( entry.name, self.is_hidden(entry.path), entry.stat().st_size ) ) except (PermissionError, FileNotFoundError): pass return (sorted(dirs), sorted(files)) def handle_post(self): if self._upload: self.handle_multipart(self._dir, self._path_only) else: super().handle_post() def handle_put(self): if self._upload: self.handle_putfile(self._dir.rstrip('/') + self._path_only) else: super().handle_put() def file_filter(self, file_path): return self._all or not self.is_hidden(file_path) def _contains_hidden_segment_windows(self, path): if path == '/': return False prefix = self._dir for segment in path.strip('/').split('/'): if self.is_hidden(prefix + segment): return True prefix = prefix + segment + '/' return False def _contains_hidden_segment_unix(self, path): return path.find('/.') != -1 class FileReceiveHandler(BaseHandler): def __init__(self, dir_path, *args, **kwargs): self._dir = dir_path super().__init__(*args, **kwargs) def handle_get(self): if not self._path_only.endswith('/'): self.respond_redirect(f'{parse.quote(self._path_only)}/') return last_modified = self.start_time if self.get_if_modified_since() == last_modified: self.respond_not_modified(last_modified) else: self.respond_with_html(self.build_html(), last_modified) def build_html(self): builder = HtmlBuilder() builder.start_head() builder.start_title() builder.append(self._hostname) builder.end_title() builder.start_style() builder.append( '.container{height: 80%; padding: 0 8px; display: flex; align-items: center; justify-content: center;}' ) builder.append('@media (prefers-color-scheme: light) {') builder.append( '.upload{width: 100%; height: 60%; border: 1px solid #cccccc; border-radius: 16px; cursor: pointer; background-color: white; color: #333333; font-size: xx-large;}' ) builder.append('.upload:hover{background-color: #e6e6e6;}') builder.append('.dragging{border: 4px dashed #cccccc;}') builder.append('}') builder.append('@media (prefers-color-scheme: dark) {') builder.append( '.upload{width: 100%; height: 60%; border: 1px solid #333333; border-radius: 16px; cursor: pointer; background-color: black; color: #cccccc; font-size: xx-large;}' ) builder.append('.upload:hover{background-color: #1a1a1a;}') builder.append('.dragging{border: 4px dashed #333333;}') builder.append('}') builder.append('.upload:disabled{opacity: .65; pointer-events: none; user-select: none;}') builder.end_style() builder.start_script() builder.append( ''' function on_upload_click() { document.getElementById("file").click(); } function on_upload() { document.getElementById("upload").setAttribute("disabled", ""); document.getElementById("form").submit(); } let drag_counter = 0; function on_dragenter(e) { e.preventDefault(); drag_counter++; if (drag_counter === 1) { let types = e.dataTransfer.types; if (types[types.length - 1] === "Files") { e.currentTarget.classList.add("dragging"); } } } function on_dragover(e) { e.preventDefault(); } function on_dragleave(e) { e.preventDefault(); drag_counter--; if (drag_counter === 0) { e.currentTarget.classList.remove("dragging"); } } function on_drop(e) { e.preventDefault(); drag_counter = 0; e.currentTarget.classList.remove("dragging"); if (e.dataTransfer.files.length > 0) { document.getElementById("file").files = e.dataTransfer.files; on_upload(); } } function on_load() { let upload = document.getElementById("upload"); upload.onclick = on_upload_click; upload.ondragenter = on_dragenter; upload.ondragover = on_dragover; upload.ondragleave = on_dragleave; upload.ondrop = on_drop; let file = document.getElementById("file"); file.onchange = on_upload; } window.onload = on_load; ''' ) builder.end_script() builder.end_head() builder.start_body() builder.append('
') builder.append('') builder.append('
') builder.end_body() return builder.build() def handle_post(self): self.handle_multipart(self._dir, self._path_only) def handle_put(self): self.handle_putfile(self._dir.rstrip('/') + self._path_only) class TextShareHandler(BaseHandler): def __init__(self, text, *args, **kwargs): self._text = text super().__init__(*args, **kwargs) def handle_get(self): if self._path_only != '/': self.respond_redirect('/') return last_modified = self.start_time if self.get_if_modified_since() == last_modified: self.respond_not_modified(last_modified) return if self.get_accept_content_type() == 'text/plain': self.respond_with_text(self.build_text(), last_modified) else: self.respond_with_html(self.build_html(), last_modified) def build_text(self): return f'{self._text}\n' def build_html(self): builder = HtmlBuilder() builder.start_head() builder.start_title() builder.append(self._hostname) builder.end_title() builder.start_style() builder.append('.container{height: 100%; display: flex; flex-direction: column;}') builder.append( '.content{flex: auto; margin: 10% 8px; word-wrap: break-word; white-space: pre-wrap; overflow-y: auto;}' ) builder.end_style() builder.start_body() builder.append('
') builder.append('
')
        builder.append(html.escape(self._text))
        builder.append('
') builder.append('
') builder.end_body() return builder.build() class TextReceiveHandler(BaseHandler): def handle_get(self): if self._path_only != '/': self.respond_redirect('/') return last_modified = self.start_time if self.get_if_modified_since() == last_modified: self.respond_not_modified(last_modified) else: self.respond_with_html(self.build_html(), last_modified) def build_html(self): builder = HtmlBuilder() builder.start_head() builder.start_title() builder.append(self._hostname) builder.end_title() builder.start_style() builder.append('.container{height: 100%; display: flex; flex-direction: column;}') builder.append( '.content{flex: auto; margin: 10% 8px; display: flex; flex-direction: column;}' ) builder.append('.textarea{flex: auto; width: 100%;}') builder.append('.submit{width: 100%;}') builder.end_style() builder.start_script() builder.append( ''' function on_keydown(e) { if (e.key === "Control") { return; } if (e.ctrlKey && e.key === "Enter") { form = document.getElementById("form"); if (form.reportValidity()) { form.submit(); e.preventDefault(); e.stopPropagation(); } } } function on_load() { text = document.getElementById("text"); text.onkeydown = on_keydown; } window.onload = on_load; ''' ) builder.end_script() builder.end_head() builder.start_body() builder.append('
') builder.append('
') builder.append( '' ) builder.append('
') builder.append('') builder.append('
') builder.append('
') builder.end_body() return builder.build() def handle_post(self): if self._path_only != '/': self.respond_bad_request() return content_type = self.headers['Content-Type'] if content_type != 'application/x-www-form-urlencoded': self.respond_bad_request() return content_length = self.get_content_length() if not content_length or content_length <= 5 or content_length > 1048576: self.respond_bad_request() return text = self.rfile.read(5).decode() if text != 'text=': self.respond_bad_request() return text = self.rfile.read(content_length - 5).decode() text = parse.unquote_plus(text) if self.get_accept_content_type() == 'text/plain': self.respond(HTTPStatus.OK, content_length='0') else: self.respond_redirect('/') print(f'From {self.client_address[0]}:{self.client_address[1]}:\n{text}') class HtmlBuilder: def __init__(self): self._list = [] def start_head(self): self._list.append('') self._list.append('') self._list.append('') self._list.append( '' ) def end_head(self): self._list.append('') def start_title(self): self._list.append('') def end_title(self): self._list.append('') def start_style(self): self._list.append('') def start_script(self): self._list.append('') def start_body(self): self._list.append('') def end_body(self): self._list.append('') def append(self, code): self._list.append(code) def build(self): self._list.append('') return ''.join(self._list) class FileItem: _name_part_pattern = re.compile(r'([0\D]+)|([1-9]\d*)', re.ASCII) def __init__(self, name, hidden, size): self.name = name self.hidden = hidden self.size = size self._name_parts = [ (string if string else number, int(number) if number else None) for string, number in self._name_part_pattern.findall(name.upper()) ] def __lt__(self, other): if self.hidden != other.hidden: return self.hidden name1, name2 = self.name, other.name if name1[0] == '.' and name2[0] != '.': return True if name1[0] != '.' and name2[0] == '.': return False len1, len2 = len(self._name_parts), len(other._name_parts) i, min_len = 0, min(len1, len2) while i < min_len: string1, number1 = self._name_parts[i] string2, number2 = other._name_parts[i] if number1 is not None and number2 is not None: if number1 == number2: i += 1 else: return number1 < number2 elif string1 == string2: i += 1 else: slen1, slen2 = len(string1), len(string2) if slen1 < slen2 and i < len1 - 1: string1 += self._name_parts[i + 1][0] if slen1 > slen2 and i < len2 - 1: string2 += other._name_parts[i + 1][0] return string1 < string2 if len1 == len2: return name1 < name2 else: return len1 < len2 class MultipartFile: _content_disposition_pattern = re.compile(r'^form-data; name="(.+)"; filename="(.+)"$') def __init__(self, headers, transfer_to): self.transfer_to = transfer_to content_disposition = headers.get('Content-Disposition') if not content_disposition: raise MultipartError match = self._content_disposition_pattern.match(content_disposition) if not match: raise MultipartError self.name = match.group(1) self.filename = os.path.basename(match.group(2)) class MultipartParser: def __init__(self, stream, boundary, content_length): self._stream = stream self._total_length = content_length self._read_length = 0 self._separator = f'--{boundary}\r\n'.encode() self._terminator = f'--{boundary}--\r\n'.encode() self._terminated = False def __iter__(self): if self._read_length != 0: raise MultipartError if self._read_line() != self._separator: raise MultipartError while not self._terminated: yield MultipartFile(self._read_headers(), self._transfer_to) if self._read_length != self._total_length: raise MultipartError def _read_line(self): if self._read_length >= self._total_length: raise MultipartError l = min(65536, self._total_length - self._read_length) line = self._stream.readline(l) if not line: raise MultipartError self._read_length += len(line) return line def _read_headers(self): headers = {} while True: line = self._read_line().decode() if line == '\r\n': break if line.endswith('\r\n'): line = line[:-2] elif line.endswith('\n'): line = line[:-1] parts = line.split(': ') if len(parts) != 2: raise MultipartError headers[parts[0]] = parts[1] if len(headers) > 100: raise MultipartError return headers def _transfer_to(self, out): line, next_line = None, None while True: if not line: line = self._read_line() if line.endswith(b'\r'): next_line = self._read_line() if next_line != b'\n': out.write(line) line = next_line continue line += next_line if line.endswith(b'\r\n'): next_line = self._read_line() if next_line == self._separator: if len(line) > 2: out.write(line[:-2]) return if next_line == self._terminator: self._terminated = True if len(line) > 2: out.write(line[:-2]) return out.write(line) line = next_line continue out.write(line) line = None class MultipartError(ValueError): pass class ChunkWriter: def __init__(self, stream): self._stream = stream self._closed = False def write(self, data): if not data: return 0 self._stream.write(f'{hex(len(data))[2:]}\r\n'.encode()) n = self._stream.write(data) self._stream.write(b'\r\n') return n def flush(self): self._stream.flush() def close(self): if self._closed: return self._closed = True self._stream.write(b'0\r\n\r\n') def __enter__(self): return self def __exit__(self, *exc): self.close() def get_best_family(host, port): info = socket.getaddrinfo(host, port, type=socket.SOCK_STREAM, flags=socket.AI_PASSIVE) family, type, proto, canonname, addr = info[0] return family, addr def get_ip(family): address = ('10.254.254.254', 0) if family == socket.AF_INET else ('ff05::', 0, 0, 0) default_ip = '127.0.0.1' if family == socket.AF_INET else '::1' try: with socket.socket(family, socket.SOCK_DGRAM) as s: s.connect(address) return s.getsockname()[0] except OSError: return default_ip def is_windows(): return os.name == 'nt' def on_interrupt(a, b): if not is_windows(): sys.stderr.write('\n') sys.exit(1) def print_prompt(): if is_windows(): sys.stderr.write('Enter your text, then press Ctrl + Z followed by the Enter key:\n') else: sys.stderr.write('Enter your text, then press Ctrl + D:\n') def create_ssl_context(certfile, keyfile=None, password=None): ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) ctx.minimum_version = ssl.TLSVersion.TLSv1_3 ctx.load_cert_chain(certfile=certfile, keyfile=keyfile, password=password) return ctx def print_qrcode(text): if sys.stderr.isatty(): try: import qrcode qr = qrcode.QRCode() qr.add_data(text) if is_windows(): qr.print_ascii(sys.stderr, False, True) else: qr.print_tty(sys.stderr) except ModuleNotFoundError: pass def start_server(address, port, certfile, keyfile, keypass, handler_class, show_qrcode): family, addr = get_best_family(address, port) ShareServer.address_family = family with ShareServer(addr, handler_class) as server: if certfile: ctx = create_ssl_context(certfile, keyfile, keypass) server.socket = ctx.wrap_socket(server.socket, server_side=True) https = True else: https = False host, port = server.socket.getsockname()[:2] ip = addr[0] if ip == '0.0.0.0' or ip == '::': ip = get_ip(family) if family == socket.AF_INET6: ip = f'[{ip}]' url = f'{"https" if https else "http"}://{ip}:{port}/' sys.stderr.write( f'Serving {"HTTPS" if https else "HTTP"} on {host} port {port} ({url}) ...\n' ) if show_qrcode: print_qrcode(url) server.serve_forever() def main(): signal.signal(signal.SIGINT, on_interrupt) parser = argparse.ArgumentParser(allow_abbrev=False, add_help=False) parser.add_argument('arguments', nargs='*', help='a directory, files or texts') general = parser.add_argument_group('general options') general.add_argument( '-b', '--bind', dest='address', default='0.0.0.0', help='bind address [default: 0.0.0.0]' ) general.add_argument('-p', '--port', type=int, default=8888, help='port [default: 8888]') general.add_argument('-s', '--share', action='store_true', help='share mode (default mode)') general.add_argument( '-r', '--receive', action='store_true', help='receive mode, can be used with -s option (only for directory)', ) general.add_argument( '-a', '--all', action='store_true', help='show all files, including hidden ones (only for directory)', ) general.add_argument( '-z', '--archive', action='store_true', help='share the directory itself as an archive (only for directory)', ) general.add_argument('-t', '--text', action='store_true', help='for text') general.add_argument( '-P', '--password', nargs='?', const=os.getenv('SHARE_PASSWORD'), help='access password, if no PASSWORD is specified, the environment variable SHARE_PASSWORD will be used', ) general.add_argument( '-A', '--auth-pattern', dest='pattern', default='*', help='glob pattern of paths requiring authentication [default: *]', ) general.add_argument('-q', '--qrcode', action='store_true', help='show the qrcode') general.add_argument('-h', '--help', action='help', help='show this help message and exit') tls = parser.add_argument_group('tls options') tls.add_argument('--certfile', help='cert file') tls.add_argument('--keyfile', help='key file') tls.add_argument('--keypass', help='key password') args = parser.parse_args() if args.password and len(args.password) < 3: raise ValueError('password is too short') if not args.receive: args.share = True if args.share and args.receive: dir_path = None if not args.arguments: dir_path = os.path.abspath(os.getcwd()) elif os.path.isdir(args.arguments[0]): dir_path = os.path.abspath(args.arguments[0]) else: raise FileNotFoundError(f'{args.arguments[0]} is not a directory') handler_class = functools.partial(DirectoryShareHandler, dir_path, args.all, upload=True) elif args.share: if args.text: if args.arguments: text = '\n'.join(args.arguments) else: print_prompt() text = ''.join(sys.stdin.readlines()) if not text: sys.exit(1) handler_class = functools.partial(TextShareHandler, text) else: dir_path, files = None, None if not args.arguments: dir_path = os.path.abspath(os.getcwd()) elif os.path.isdir(args.arguments[0]): dir_path = os.path.abspath(args.arguments[0]) else: for f in args.arguments: if not os.path.isfile(f): raise FileNotFoundError(f'{f} is not a file') files = [os.path.abspath(f) for f in args.arguments] if dir_path: if args.archive: handler_class = functools.partial(VirtualTarShareHandler, dir_path, args.all) else: handler_class = functools.partial(DirectoryShareHandler, dir_path, args.all) else: handler_class = functools.partial(FileShareHandler, files) else: if args.text: handler_class = TextReceiveHandler else: dir_path = None if not args.arguments: dir_path = os.path.abspath(os.getcwd()) elif os.path.isdir(args.arguments[0]): dir_path = os.path.abspath(args.arguments[0]) else: raise FileNotFoundError(f'{args.arguments[0]} is not a directory') handler_class = functools.partial(FileReceiveHandler, dir_path) BaseHandler.password = args.password if args.password: BaseHandler.encoded_password = base64.b64encode( hashlib.sha256(f'share-{args.password}'.encode()).digest() ).decode() BaseHandler.auth_pattern = re.compile(fnmatch.translate(args.pattern.removeprefix('!'))) BaseHandler.invert_match = args.pattern.startswith('!') start_server( args.address, args.port, args.certfile, args.keyfile, args.keypass, handler_class, args.qrcode, ) main()