FMT: pypiserver/_app.py
This commit is contained in:
parent
a52c0d6f4c
commit
957538a260
|
@ -8,7 +8,15 @@ import xml.dom.minidom
|
||||||
|
|
||||||
from . import __version__
|
from . import __version__
|
||||||
from . import core
|
from . import core
|
||||||
from .bottle import static_file, redirect, request, response, HTTPError, Bottle, template
|
from .bottle import (
|
||||||
|
static_file,
|
||||||
|
redirect,
|
||||||
|
request,
|
||||||
|
response,
|
||||||
|
HTTPError,
|
||||||
|
Bottle,
|
||||||
|
template,
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import xmlrpc.client as xmlrpclib # py3
|
import xmlrpc.client as xmlrpclib # py3
|
||||||
|
@ -40,7 +48,6 @@ class auth(object):
|
||||||
self.action = action
|
self.action = action
|
||||||
|
|
||||||
def __call__(self, method):
|
def __call__(self, method):
|
||||||
|
|
||||||
def protector(*args, **kwargs):
|
def protector(*args, **kwargs):
|
||||||
if self.action in config.authenticated:
|
if self.action in config.authenticated:
|
||||||
if not request.auth or request.auth[1] is None:
|
if not request.auth or request.auth[1] is None:
|
||||||
|
@ -54,25 +61,32 @@ class auth(object):
|
||||||
return protector
|
return protector
|
||||||
|
|
||||||
|
|
||||||
@app.hook('before_request')
|
@app.hook("before_request")
|
||||||
def log_request():
|
def log_request():
|
||||||
log.info(config.log_req_frmt, request.environ)
|
log.info(config.log_req_frmt, request.environ)
|
||||||
|
|
||||||
|
|
||||||
@app.hook('before_request')
|
@app.hook("before_request")
|
||||||
def print_request():
|
def print_request():
|
||||||
parsed = urlparse(request.urlparts.scheme + "://" + request.urlparts.netloc)
|
parsed = urlparse(request.urlparts.scheme + "://" + request.urlparts.netloc)
|
||||||
request.custom_host = parsed.netloc
|
request.custom_host = parsed.netloc
|
||||||
request.custom_fullpath = parsed.path.rstrip('/') + '/' + request.fullpath.lstrip('/')
|
request.custom_fullpath = (
|
||||||
|
parsed.path.rstrip("/") + "/" + request.fullpath.lstrip("/")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.hook('after_request')
|
@app.hook("after_request")
|
||||||
def log_response():
|
def log_response():
|
||||||
log.info(config.log_res_frmt, { # vars(response)) ## DOES NOT WORK!
|
log.info(
|
||||||
'response': response,
|
config.log_res_frmt,
|
||||||
'status': response.status, 'headers': response.headers,
|
{ # vars(response)) ## DOES NOT WORK!
|
||||||
'body': response.body, 'cookies': response._cookies,
|
"response": response,
|
||||||
})
|
"status": response.status,
|
||||||
|
"headers": response.headers,
|
||||||
|
"body": response.body,
|
||||||
|
"cookies": response._cookies,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.error
|
@app.error
|
||||||
|
@ -85,7 +99,7 @@ def favicon():
|
||||||
return HTTPError(404)
|
return HTTPError(404)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/')
|
@app.route("/")
|
||||||
def root():
|
def root():
|
||||||
fp = request.custom_fullpath
|
fp = request.custom_fullpath
|
||||||
|
|
||||||
|
@ -95,16 +109,18 @@ def root():
|
||||||
numpkgs = 0
|
numpkgs = 0
|
||||||
|
|
||||||
# Ensure template() does not consider `msg` as filename!
|
# Ensure template() does not consider `msg` as filename!
|
||||||
msg = config.welcome_msg + '\n'
|
msg = config.welcome_msg + "\n"
|
||||||
return template(msg,
|
return template(
|
||||||
URL=request.url.rstrip("/") + '/',
|
msg,
|
||||||
|
URL=request.url.rstrip("/") + "/",
|
||||||
VERSION=__version__,
|
VERSION=__version__,
|
||||||
NUMPKGS=numpkgs,
|
NUMPKGS=numpkgs,
|
||||||
PACKAGES=fp.rstrip("/") + "/packages/",
|
PACKAGES=fp.rstrip("/") + "/packages/",
|
||||||
SIMPLE=fp.rstrip("/") + "/simple/"
|
SIMPLE=fp.rstrip("/") + "/simple/",
|
||||||
)
|
)
|
||||||
|
|
||||||
_bottle_upload_filename_re = re.compile(r'^[a-z0-9_.!+-]+$', re.I)
|
|
||||||
|
_bottle_upload_filename_re = re.compile(r"^[a-z0-9_.!+-]+$", re.I)
|
||||||
|
|
||||||
|
|
||||||
def is_valid_pkg_filename(fname):
|
def is_valid_pkg_filename(fname):
|
||||||
|
@ -114,13 +130,13 @@ def is_valid_pkg_filename(fname):
|
||||||
|
|
||||||
def doc_upload():
|
def doc_upload():
|
||||||
try:
|
try:
|
||||||
content = request.files['content']
|
content = request.files["content"]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise HTTPError(400, "Missing 'content' file-field!")
|
raise HTTPError(400, "Missing 'content' file-field!")
|
||||||
zip_data = content.file.read()
|
zip_data = content.file.read()
|
||||||
try:
|
try:
|
||||||
zf = zipfile.ZipFile(BytesIO(zip_data))
|
zf = zipfile.ZipFile(BytesIO(zip_data))
|
||||||
zf.getinfo('index.html')
|
zf.getinfo("index.html")
|
||||||
except Exception:
|
except Exception:
|
||||||
raise HTTPError(400, "not a zip file")
|
raise HTTPError(400, "not a zip file")
|
||||||
|
|
||||||
|
@ -141,47 +157,61 @@ def remove_pkg():
|
||||||
os.unlink(found.fn)
|
os.unlink(found.fn)
|
||||||
|
|
||||||
|
|
||||||
Upload = namedtuple('Upload', 'pkg sig')
|
Upload = namedtuple("Upload", "pkg sig")
|
||||||
|
|
||||||
|
|
||||||
def file_upload():
|
def file_upload():
|
||||||
ufiles = Upload._make(
|
ufiles = Upload._make(
|
||||||
request.files.get(f, None) for f in ('content', 'gpg_signature'))
|
request.files.get(f, None) for f in ("content", "gpg_signature")
|
||||||
|
)
|
||||||
if not ufiles.pkg:
|
if not ufiles.pkg:
|
||||||
raise HTTPError(400, "Missing 'content' file-field!")
|
raise HTTPError(400, "Missing 'content' file-field!")
|
||||||
if (ufiles.sig and
|
if (
|
||||||
'%s.asc' % ufiles.pkg.raw_filename != ufiles.sig.raw_filename):
|
ufiles.sig
|
||||||
raise HTTPError(400, "Unrelated signature %r for package %r!",
|
and "%s.asc" % ufiles.pkg.raw_filename != ufiles.sig.raw_filename
|
||||||
ufiles.sig, ufiles.pkg)
|
):
|
||||||
|
raise HTTPError(
|
||||||
|
400,
|
||||||
|
"Unrelated signature %r for package %r!",
|
||||||
|
ufiles.sig,
|
||||||
|
ufiles.pkg,
|
||||||
|
)
|
||||||
|
|
||||||
for uf in ufiles:
|
for uf in ufiles:
|
||||||
if not uf:
|
if not uf:
|
||||||
continue
|
continue
|
||||||
if (not is_valid_pkg_filename(uf.raw_filename) or
|
if (
|
||||||
core.guess_pkgname_and_version(uf.raw_filename) is None):
|
not is_valid_pkg_filename(uf.raw_filename)
|
||||||
|
or core.guess_pkgname_and_version(uf.raw_filename) is None
|
||||||
|
):
|
||||||
raise HTTPError(400, "Bad filename: %s" % uf.raw_filename)
|
raise HTTPError(400, "Bad filename: %s" % uf.raw_filename)
|
||||||
|
|
||||||
if not config.overwrite and core.exists(packages.root, uf.raw_filename):
|
if not config.overwrite and core.exists(packages.root, uf.raw_filename):
|
||||||
log.warn("Cannot upload %r since it already exists! \n"
|
log.warn(
|
||||||
|
"Cannot upload %r since it already exists! \n"
|
||||||
" You may start server with `--overwrite` option. ",
|
" You may start server with `--overwrite` option. ",
|
||||||
uf.raw_filename)
|
uf.raw_filename,
|
||||||
raise HTTPError(409, "Package %r already exists!\n"
|
)
|
||||||
|
raise HTTPError(
|
||||||
|
409,
|
||||||
|
"Package %r already exists!\n"
|
||||||
" You may start server with `--overwrite` option.",
|
" You may start server with `--overwrite` option.",
|
||||||
uf.raw_filename)
|
uf.raw_filename,
|
||||||
|
)
|
||||||
|
|
||||||
core.store(packages.root, uf.raw_filename, uf.save)
|
core.store(packages.root, uf.raw_filename, uf.save)
|
||||||
if request.auth:
|
if request.auth:
|
||||||
user = request.auth[0]
|
user = request.auth[0]
|
||||||
else:
|
else:
|
||||||
user = 'anon'
|
user = "anon"
|
||||||
log.info('User %r stored %r.', user, uf.raw_filename)
|
log.info("User %r stored %r.", user, uf.raw_filename)
|
||||||
|
|
||||||
|
|
||||||
@app.post('/')
|
@app.post("/")
|
||||||
@auth("update")
|
@auth("update")
|
||||||
def update():
|
def update():
|
||||||
try:
|
try:
|
||||||
action = request.forms[':action']
|
action = request.forms[":action"]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise HTTPError(400, "Missing ':action' field!")
|
raise HTTPError(400, "Missing ':action' field!")
|
||||||
|
|
||||||
|
@ -201,35 +231,46 @@ def update():
|
||||||
|
|
||||||
@app.route("/simple")
|
@app.route("/simple")
|
||||||
@app.route("/simple/:prefix")
|
@app.route("/simple/:prefix")
|
||||||
@app.route('/packages')
|
@app.route("/packages")
|
||||||
@auth("list")
|
@auth("list")
|
||||||
def pep_503_redirects(prefix=None):
|
def pep_503_redirects(prefix=None):
|
||||||
return redirect(request.custom_fullpath + "/", 301)
|
return redirect(request.custom_fullpath + "/", 301)
|
||||||
|
|
||||||
|
|
||||||
@app.post('/RPC2')
|
@app.post("/RPC2")
|
||||||
@auth("list")
|
@auth("list")
|
||||||
def handle_rpc():
|
def handle_rpc():
|
||||||
"""Handle pip-style RPC2 search requests"""
|
"""Handle pip-style RPC2 search requests"""
|
||||||
parser = xml.dom.minidom.parse(request.body)
|
parser = xml.dom.minidom.parse(request.body)
|
||||||
methodname = parser.getElementsByTagName(
|
methodname = (
|
||||||
"methodName")[0].childNodes[0].wholeText.strip()
|
parser.getElementsByTagName("methodName")[0]
|
||||||
|
.childNodes[0]
|
||||||
|
.wholeText.strip()
|
||||||
|
)
|
||||||
log.info("Processing RPC2 request for '%s'", methodname)
|
log.info("Processing RPC2 request for '%s'", methodname)
|
||||||
if methodname == 'search':
|
if methodname == "search":
|
||||||
value = parser.getElementsByTagName(
|
value = (
|
||||||
"string")[0].childNodes[0].wholeText.strip()
|
parser.getElementsByTagName("string")[0]
|
||||||
|
.childNodes[0]
|
||||||
|
.wholeText.strip()
|
||||||
|
)
|
||||||
response = []
|
response = []
|
||||||
ordering = 0
|
ordering = 0
|
||||||
for p in packages():
|
for p in packages():
|
||||||
if p.pkgname.count(value) > 0:
|
if p.pkgname.count(value) > 0:
|
||||||
# We do not presently have any description/summary, returning
|
# We do not presently have any description/summary, returning
|
||||||
# version instead
|
# version instead
|
||||||
d = {'_pypi_ordering': ordering, 'version': p.version,
|
d = {
|
||||||
'name': p.pkgname, 'summary': p.version}
|
"_pypi_ordering": ordering,
|
||||||
|
"version": p.version,
|
||||||
|
"name": p.pkgname,
|
||||||
|
"summary": p.version,
|
||||||
|
}
|
||||||
response.append(d)
|
response.append(d)
|
||||||
ordering += 1
|
ordering += 1
|
||||||
call_string = xmlrpclib.dumps((response,), 'search',
|
call_string = xmlrpclib.dumps(
|
||||||
methodresponse=True)
|
(response,), "search", methodresponse=True
|
||||||
|
)
|
||||||
return call_string
|
return call_string
|
||||||
|
|
||||||
|
|
||||||
|
@ -259,19 +300,29 @@ def simple(prefix=""):
|
||||||
# PEP 503: require normalized prefix
|
# PEP 503: require normalized prefix
|
||||||
normalized = core.normalize_pkgname_for_url(prefix)
|
normalized = core.normalize_pkgname_for_url(prefix)
|
||||||
if prefix != normalized:
|
if prefix != normalized:
|
||||||
return redirect('/simple/{0}/'.format(normalized), 301)
|
return redirect("/simple/{0}/".format(normalized), 301)
|
||||||
|
|
||||||
files = sorted(core.find_packages(packages(), prefix=prefix),
|
files = sorted(
|
||||||
key=lambda x: (x.parsed_version, x.relfn))
|
core.find_packages(packages(), prefix=prefix),
|
||||||
|
key=lambda x: (x.parsed_version, x.relfn),
|
||||||
|
)
|
||||||
if not files:
|
if not files:
|
||||||
if config.redirect_to_fallback:
|
if config.redirect_to_fallback:
|
||||||
return redirect("%s/%s/" % (config.fallback_url.rstrip("/"), prefix))
|
return redirect(
|
||||||
return HTTPError(404, 'Not Found (%s does not exist)\n\n' % normalized)
|
"%s/%s/" % (config.fallback_url.rstrip("/"), prefix)
|
||||||
|
)
|
||||||
|
return HTTPError(404, "Not Found (%s does not exist)\n\n" % normalized)
|
||||||
|
|
||||||
fp = request.custom_fullpath
|
fp = request.custom_fullpath
|
||||||
links = [(os.path.basename(f.relfn),
|
links = [
|
||||||
urljoin(fp, "../../packages/%s" % f.fname_and_hash(config.hash_algo)))
|
(
|
||||||
for f in files]
|
os.path.basename(f.relfn),
|
||||||
|
urljoin(
|
||||||
|
fp, "../../packages/%s" % f.fname_and_hash(config.hash_algo)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
for f in files
|
||||||
|
]
|
||||||
tmpl = """\
|
tmpl = """\
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
@ -288,16 +339,18 @@ def simple(prefix=""):
|
||||||
return template(tmpl, prefix=prefix, links=links)
|
return template(tmpl, prefix=prefix, links=links)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/packages/')
|
@app.route("/packages/")
|
||||||
@auth("list")
|
@auth("list")
|
||||||
def list_packages():
|
def list_packages():
|
||||||
fp = request.custom_fullpath
|
fp = request.custom_fullpath
|
||||||
files = sorted(core.find_packages(packages()),
|
files = sorted(
|
||||||
key=lambda x: (os.path.dirname(x.relfn),
|
core.find_packages(packages()),
|
||||||
x.pkgname,
|
key=lambda x: (os.path.dirname(x.relfn), x.pkgname, x.parsed_version),
|
||||||
x.parsed_version))
|
)
|
||||||
links = [(f.relfn_unix, urljoin(fp, f.fname_and_hash(config.hash_algo)))
|
links = [
|
||||||
for f in files]
|
(f.relfn_unix, urljoin(fp, f.fname_and_hash(config.hash_algo)))
|
||||||
|
for f in files
|
||||||
|
]
|
||||||
tmpl = """\
|
tmpl = """\
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
@ -314,7 +367,7 @@ def list_packages():
|
||||||
return template(tmpl, links=links)
|
return template(tmpl, links=links)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/packages/:filename#.*#')
|
@app.route("/packages/:filename#.*#")
|
||||||
@auth("download")
|
@auth("download")
|
||||||
def server_static(filename):
|
def server_static(filename):
|
||||||
entries = core.find_packages(packages())
|
entries = core.find_packages(packages())
|
||||||
|
@ -322,17 +375,21 @@ def server_static(filename):
|
||||||
f = x.relfn_unix
|
f = x.relfn_unix
|
||||||
if f == filename:
|
if f == filename:
|
||||||
response = static_file(
|
response = static_file(
|
||||||
filename, root=x.root, mimetype=mimetypes.guess_type(filename)[0])
|
filename,
|
||||||
|
root=x.root,
|
||||||
|
mimetype=mimetypes.guess_type(filename)[0],
|
||||||
|
)
|
||||||
if config.cache_control:
|
if config.cache_control:
|
||||||
response.set_header(
|
response.set_header(
|
||||||
"Cache-Control", "public, max-age=%s" % config.cache_control)
|
"Cache-Control", "public, max-age=%s" % config.cache_control
|
||||||
|
)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
return HTTPError(404, 'Not Found (%s does not exist)\n\n' % filename)
|
return HTTPError(404, "Not Found (%s does not exist)\n\n" % filename)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/:prefix')
|
@app.route("/:prefix")
|
||||||
@app.route('/:prefix/')
|
@app.route("/:prefix/")
|
||||||
def bad_url(prefix):
|
def bad_url(prefix):
|
||||||
"""Redirect unknown root URLs to /simple/."""
|
"""Redirect unknown root URLs to /simple/."""
|
||||||
return redirect(core.get_bad_url_redirect_path(request, prefix))
|
return redirect(core.get_bad_url_redirect_path(request, prefix))
|
||||||
|
|
Loading…
Reference in New Issue