#!/usr/bin/env python3
# SPDX-License-Identifier: WTFPL

# /// script
# dependencies = ["bottle"]
# ///

"""
Shares a directory on HTTP like "python3 -m http.server" but is capable of
handling "Range" header, making it suitable for seeking randomly in medias.
"""

import argparse
from pathlib import Path
import os
from socketserver import ThreadingMixIn
from urllib.parse import quote
from wsgiref.simple_server import WSGIServer

from bottle import (
	route, run, abort, static_file, WSGIRefServer, redirect,
	request, HTTPError, install,
)


DIR = """<!DOCTYPE html>
<html>
<body>
<ul>
{0}
</ul>
</body>
</html>
"""


class BasicAuthPlugin:
	api = 2
	name = "require-basic-auth"

	def __init__(self, required_auth):
		if not required_auth is None:
			# just check type
			_, _ = required_auth
		self.required_auth = required_auth

	def setup(self, app):
		pass

	def apply(self, callback, route):
		def wrapper(*args, **kwargs):
			if self.required_auth and request.auth != self.required_auth:
				return HTTPError(
					401, 'Authentication required',
					**{'WWW-Authenticate': 'Basic realm="Private"'}
				)
			return callback(*args, **kwargs)

		return wrapper


@route('/')
@route('/<path:path>')
def anything(path='/'):
	try:
		target = ROOT.joinpath(path.lstrip('/')).resolve(True)
		relative = target.relative_to(ROOT)
	except (FileNotFoundError, ValueError) as exc:
		abort(403)

	if target.is_dir():
		if not path.endswith('/'):
			redirect(f'{path}/')

		parts = [
			f'<li><a href="{quote(sub.name)}{"/" if sub.is_dir() else ""}">'
			f'{sub.name}{"/" if sub.is_dir() else ""}</a></li>'
			for sub in sorted(target.iterdir())
		]
		return DIR.format('\n'.join(parts))
	elif target.is_file():
		return static_file(str(relative), str(ROOT))

	abort(404)


class ThreadedServer(ThreadingMixIn, WSGIServer):
	pass


def parse_auth(s):
	if s == "env":
		try:
			return (
				os.environ["HTTPSHARE_USER"],
				os.environ["HTTPSHARE_PASSWORD"]
			)
		except KeyError:
			raise ValueError(
				"Missing HTTPSHARE_USER or HTTPSHARE_PASSWORD"
			)
	user, sep, password = s.partition(":")
	if not sep:
		raise ValueError("Format is USER:PASSWORD")
	return (user, password)


parser = argparse.ArgumentParser()
parser.add_argument('--bind', '-b', default='', metavar='ADDRESS',
	help='Specify alternate bind address '
	'[default: all interfaces]')
parser.add_argument('--directory', '-d', default=Path.cwd(), type=Path,
	help='Specify alternative directory [default:current directory]'
)
parser.add_argument(
	'--auth', metavar='USER:PASSWORD', type=parse_auth,
	help='Require HTTP authentication',
)
parser.add_argument('port', action='store',
	default=8000, type=int,
	nargs='?',
	help='Specify alternate port [default: 8000]')
args = parser.parse_args()

ROOT = args.directory

install(BasicAuthPlugin(args.auth))
run(server=WSGIRefServer(host=args.bind, port=args.port, server_class=ThreadedServer))
