Merge branch 'allow-asc-uploads' into dev.

This commit is contained in:
Kostis Anagnostopoulos 2016-01-19 20:10:22 +01:00
commit 27a0b05433
4 changed files with 113 additions and 63 deletions

@ -1,10 +1,15 @@
import os
import zipfile
import mimetypes
from collections import namedtuple
import logging
import mimetypes
import os
import re
import sys
import zipfile
from . import __version__
from . import core
from .bottle import static_file, redirect, request, response, HTTPError, Bottle, template
try:
from io import BytesIO
@ -16,8 +21,6 @@ try: # PY3
except ImportError: # PY2
from urlparse import urljoin
from .bottle import static_file, redirect, request, response, HTTPError, Bottle, template
from . import __version__
log = logging.getLogger(__name__)
packages = None
@ -91,11 +94,73 @@ def root():
)
_bottle_upload_filename_re = re.compile(r'^[a-z0-9_.!+-]+$', re.I)
def is_valid_pkg_filename(fname):
"""See https://github.com/pypiserver/pypiserver/issues/102"""
return _bottle_upload_filename_re.match(fname) is not None
def doc_upload():
try:
content = request.files['content']
except KeyError:
raise HTTPError(400, "Missing 'content' file-field!")
zip_data = content.file.read()
try:
zf = zipfile.ZipFile(BytesIO(zip_data))
zf.getinfo('index.html')
except Exception:
raise HTTPError(400, "not a zip file")
def remove_pkg():
name = request.forms.get("name")
version = request.forms.get("version")
if not name or not version:
msg = "Missing 'name'/'version' fields: name=%s, version=%s"
raise HTTPError(400, msg % (name, version))
found = None
for pkg in core.find_packages(packages()):
if pkg.pkgname == name and pkg.version == version:
found = pkg
break
if found is None:
raise HTTPError(404, "%s (%s) not found" % (name, version))
os.unlink(found.fn)
Upload = namedtuple('Upload', 'pkg sig')
def file_upload():
ufiles = Upload._make(
request.files.get(f, None) for f in ('content', 'gpg_signature'))
if not ufiles.pkg:
raise HTTPError(400, "Missing 'content' file-field!")
if (ufiles.sig and
'%s.asc' % ufiles.pkg.raw_filename != ufiles.sig.raw_filename):
raise HTTPError(400, "Unrelated signature %r for package %r!",
ufiles.sig, ufiles.pkg)
for uf in ufiles:
if not uf:
continue
if (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)
if not config.overwrite and core.exists(packages.root, uf.raw_filename):
log.warn("Cannot upload %r since it already exists! \n"
" You may start server with `--overwrite` option. ",
uf.raw_filename)
HTTPError(409, "Package %r already exists!\n"
" You may start server with `--overwrite` option.",
uf.raw_filename)
core.store(packages.root, uf.raw_filename, uf.save)
@app.post('/')
@auth("update")
def update():
@ -105,57 +170,16 @@ def update():
raise HTTPError(400, "Missing ':action' field!")
if action in ("verify", "submit"):
return ""
if action == "doc_upload":
try:
content = request.files['content']
except KeyError:
raise HTTPError(400, "Missing 'content' file-field!")
zip_data = content.file.read()
try:
zf = zipfile.ZipFile(BytesIO(zip_data))
zf.getinfo('index.html')
except Exception:
raise HTTPError(400, "not a zip file")
return ""
if action == "remove_pkg":
name = request.forms.get("name")
version = request.forms.get("version")
if not name or not version:
msg = "Missing 'name'/'version' fields: name=%s, version=%s"
raise HTTPError(400, msg % (name, version))
found = None
for pkg in core.find_packages(packages()):
if pkg.pkgname == name and pkg.version == version:
found = pkg
break
if found is None:
raise HTTPError(404, "%s (%s) not found" % (name, version))
os.unlink(found.fn)
return ""
if action != "file_upload":
log.warning("Ignored ':action': %s", action)
elif action == "doc_upload":
doc_upload()
elif action == "remove_pkg":
remove_pkg()
elif action == "file_upload":
file_upload()
else:
raise HTTPError(400, "Unsupported ':action' field: %s" % action)
try:
content = request.files['content']
except KeyError:
raise HTTPError(400, "Missing 'content' file-field!")
if (not is_valid_pkg_filename(content.raw_filename) or
core.guess_pkgname_and_version(content.raw_filename) is None):
raise HTTPError(400, "Bad filename: %s" % content.raw_filename)
if not config.overwrite and core.exists(packages.root, content.raw_filename):
log.warn("Cannot upload package(%s) since it already exists! \n" +
" You may use `--overwrite` option when starting server to disable this check. ",
content.raw_filename)
msg = "Package already exists! Start server with `--overwrite` option?"
raise HTTPError(409, msg)
core.store(packages.root, content.raw_filename, content.save)
return ""
@ -203,7 +227,7 @@ def simple(prefix=""):
links = [(os.path.basename(f.relfn),
urljoin(fp, "../../packages/%s#%s" % (f.relfn_unix,
f.hash(config.hash_algo))))
f.hash(config.hash_algo))))
for f in files]
tmpl = """\
<html>
@ -230,11 +254,11 @@ def list_packages():
fp += "/"
files = sorted(core.find_packages(packages()),
key=lambda x: (os.path.dirname(x.relfn),
x.pkgname,
x.parsed_version))
key=lambda x: (os.path.dirname(x.relfn),
x.pkgname,
x.parsed_version))
links = [(f.relfn_unix, '%s#%s' % (urljoin(fp, f.relfn),
f.hash(config.hash_algo)))
f.hash(config.hash_algo)))
for f in files]
tmpl = """\
<html>

@ -1,10 +1,11 @@
#! /usr/bin/env python
"""minimal PyPI like server for use with pip/easy_install"""
from collections import namedtuple
import functools
import hashlib
import io
import itertools
import functools
import logging
import mimetypes
import os
@ -12,8 +13,10 @@ import re
import sys
import pkg_resources
from . import Configuration
log = logging.getLogger(__name__)
@ -91,6 +94,7 @@ def auth_by_htpasswd_file(htPsswdFile, username, password):
mimetypes.add_type("application/octet-stream", ".egg")
mimetypes.add_type("application/octet-stream", ".whl")
mimetypes.add_type("text/plain", ".asc")
#### Next 2 functions adapted from :mod:`distribute.pkg_resources`.
@ -155,6 +159,8 @@ def _guess_pkgname_and_version_wheel(basename):
def guess_pkgname_and_version(path):
path = os.path.basename(path)
if path.endswith(".asc"):
path = path.rstrip(".asc")
if path.endswith(".whl"):
return _guess_pkgname_and_version_wheel(path)
if not _archive_suffix_rx.search(path):
@ -280,10 +286,9 @@ def exists(root, filename):
def store(root, filename, save_method):
assert "/" not in filename
dest_fn = os.path.join(root, filename)
save_method(dest_fn, overwrite=True) # Overwite check elsewhere.
save_method(dest_fn, overwrite=True) # Overwite check earlier.
log.info("Stored %r.", filename)
log.info("Stored package: %s", filename)
return True
def digest_file(fpath, hash_algo):
"""

@ -334,6 +334,20 @@ def test_upload(package, root, testapp):
assert len(uploaded_pkgs) == 1
assert uploaded_pkgs[0].lower() == package.lower()
@pytest.mark.parametrize(("package"), [f[0]
for f in test_core.files
if f[1] and '/' not in f[0]])
def test_upload_with_signature(package, root, testapp):
resp = testapp.post("/", params={':action': 'file_upload'},
upload_files=[
('content', package, b''),
('gpg_signature', '%s.asc' % package, b'')])
assert resp.status_int == 200
uploaded_pkgs = [f.basename for f in root.listdir()]
assert len(uploaded_pkgs) == 2
assert uploaded_pkgs[0].lower() == package.lower()
assert uploaded_pkgs[1].lower() == '%s.asc' % package.lower()
@pytest.mark.parametrize(("package"), [
f[0] for f in test_core.files
if f[1] is None])

@ -54,6 +54,7 @@ files = [
("pkg.zip", 'pkg', ''),
("foo/pkg.zip", 'pkg', ''),
("foo/pkg-1b.zip", 'pkg', '1b'),
("package-name-0.0.1.alpha.1.win-amd64-py3.2.exe", "package-name", "0.0.1.alpha.1"),
]
def _capitalize_ext(fpath):
@ -68,6 +69,12 @@ def test_guess_pkgname_and_version(filename, pkgname, version):
assert core.guess_pkgname_and_version(filename) == exp
assert core.guess_pkgname_and_version(_capitalize_ext(filename)) == exp
@pytest.mark.parametrize(("filename", "pkgname", "version"), files)
def test_guess_pkgname_and_version_asc(filename, pkgname, version):
exp = (pkgname, version)
filename = '%s.asc' % filename
assert core.guess_pkgname_and_version(filename) == exp
def test_listdir_bad_name(tmpdir):
tmpdir.join("foo.whl").ensure()