forked from github.com/pypiserver
Merge branch 'allow-asc-uploads' into dev.
This commit is contained in:
commit
27a0b05433
@ -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()
|
||||
|
Loading…
Reference in New Issue
Block a user