def parse_nuspec(nuspec, ns=None): """ Parameters ---------- nuspec : :class:`lxml.etree.Element` object The parsed nuspec data. ns : string The namespace to search. Returns ------- metadata : :class:`lxml.etree.Element` pkg_name : str version : str """ metadata = nuspec.find('nuspec:metadata', ns) if metadata is None: msg = 'Unable to find the metadata tag!' logger.error(msg) raise ApiException(msg) # TODO: I think I need different error handling around `.text`. pkg_name = metadata.find('nuspec:id', ns) version = metadata.find('nuspec:version', ns) if pkg_name is None or version is None: logger.error("ID or version missing from NuSpec file.") raise ApiException("api_error: ID or version missing") # TODO return metadata, pkg_name.text, version.text
def require_auth(headers): """Ensure that the API key is valid.""" key = headers.get('X-Nuget-Apikey', None) is_valid = key is not None and key in current_app.config['API_KEYS'] if not is_valid: logger.error("Missing or Invalid API key") return is_valid
def extract_nuspec(file): """ Parameters ---------- file : :class:`pathlib.Path` object or str The file as retrieved by Flask. """ pkg = ZipFile(str(file), 'r') logger.debug("Parsing uploaded file.") nuspec_file = None pattern = re.compile(r'^.*\.nuspec$', re.IGNORECASE) nuspec_file = list(filter(pattern.search, pkg.namelist())) if len(nuspec_file) > 1: logger.error("Multiple NuSpec files found within the package.") raise ApiException("api_error: multiple nuspec files found") elif len(nuspec_file) == 0: logger.error("No NuSpec file found in the package.") raise ApiException("api_error: nuspec file not found") # TODO nuspec_file = nuspec_file[0] with pkg.open(nuspec_file, 'r') as openf: nuspec_string = openf.read() logger.debug("NuSpec string:") logger.debug(nuspec_string) logger.debug("Parsing NuSpec file XML") nuspec = et.fromstring(nuspec_string) if not et.iselement(nuspec): msg = "`nuspec` expected to be type `xml...Element`. Got {}" raise TypeError(msg.format(type(nuspec))) return nuspec
def save_file(file, pkg_name, version): """ Parameters ---------- file : :class:`werkzeug.datastructures.FileStorage` object The file as retrieved by Flask. pkg_name : str version : str Returns: -------- local_path : :class:`pathlib.Path` The path to the saved file. """ # Save the package file to the local package dir. Thus far it's # just been floating around in magic Flask land. server_path = Path(current_app.config['SERVER_PATH']) package_dir = Path(current_app.config['PACKAGE_DIR']) local_path = server_path / package_dir local_path = local_path / pkg_name / (version + ".nupkg") # Check if the package's directory already exists. Create if needed. create_parent_dirs(local_path) logger.debug("Saving uploaded file to filesystem.") try: file.save(str(local_path)) except Exception as err: # TODO: specify exceptions logger.error("Unknown exception: %s" % err) raise err else: logger.info("Succesfully saved package to '%s'" % str(local_path)) return local_path
def find_by_id(func_args=None): """ Used by `nuget install`. It looks like the NuGet client expects this to send back a list of all versions for the package, and then the client handles extraction of an individual version. Note that this is different from the newer API /Packages(Id='pkg_name',Version='0.1.3') which appears to move the version selection to server-side. """ logger.debug("Route: /find_by_id") logger.debug(" args: {}".format(request.args)) logger.debug(" header: {}".format(request.headers)) if func_args is not None: # Using the newer API logger.debug(func_args) # Parse the args. From what I can tell, the only things sent here are: # 'Id': the name of the package # 'Version': the version string. # Looks like: # "Id='NuGetTest',Version='0.0.2'" # Naive implementation regex = re.compile(r"^Id='(?P<name>.+)',Version='(?P<version>.+)'$") match = regex.search(func_args) if match is None: msg = "Unable to parse the arg string `{}`!" logger.error(msg.format(func_args)) return msg.format(func_args), 500 pkg_name = match.group('name') version = match.group('version') logger.debug("{}, {}".format(pkg_name, version)) else: # old API pkg_name = request.args.get('id') sem_ver_level = request.args.get('semVerLevel', default=None) version = None # Some terms are quoted pkg_name = pkg_name.strip("'") results = db.find_by_pkg_name(session, pkg_name, version) logger.debug(results) feed = FeedWriter('FindPackagesById', request.url_root) resp = make_response(feed.write_to_output(results)) resp.headers['Content-Type'] # This will spam logs! #logger.debug(resp.data.decode('utf-8').replace('><',' >\n<')) return resp
def push(): """ Used by `nuget push`. """ logger.debug("push()") logger.debug(" args: {}".format(request.args)) logger.debug(" header: {}".format(request.headers)) if not core.require_auth(request.headers): return "api_error: Missing or Invalid API key", 401 logger.debug("Checking for uploaded file.") if 'package' not in request.files: logger.error("Package file was not uploaded.") return "error: File not uploaded", 409 file = request.files['package'] # Save the file to a temporary location file = core.save_file(file, "_temp", str(uuid4())) # Open the zip file that was sent and extract out the .nuspec file." try: nuspec = core.extract_nuspec(file) except Exception as err: logger.error("Exception: %s" % err) return "api_error: Zero or multiple nuspec files found", 400 # The NuSpec XML file uses namespaces. ns = {'nuspec': core.extract_namespace(nuspec)} # Make sure both the ID and the version are provided in the .nuspec file. try: metadata, pkg_name, version = core.parse_nuspec(nuspec, ns) except ApiException as err: return str(err), 400 except Exception as err: logger.error(err) return str(err), 400 valid_id = re.compile('^[A-Z0-9\.\~\+\_\-]+$', re.IGNORECASE) # Make sure that the ID and version are sane if not re.match(valid_id, pkg_name) or not re.match(valid_id, version): logger.error("Invalid ID or version.") return "api_error: Invlaid ID or Version", 400 # and that we don't already have that ID+version in our database if db.validate_id_and_version(session, pkg_name, version): logger.error("Package %s version %s already exists" % (pkg_name, version)) return "api_error: Package version already exists", 409 # Hash the uploaded file and encode the hash in Base64. For some reason. try: # rename our file. # Check if the package's directory already exists. Create if needed. new = file.parent.parent / pkg_name / (version + ".nupkg") core.create_parent_dirs(new) logger.debug("Renaming %s to %s" % (str(file), new)) file.rename(new) hash_, filesize = core.hash_and_encode_file(str(new)) except Exception as err: logger.error("Exception: %s" % err) return "api_error: Unable to save file", 500 try: dependencies = core.determine_dependencies(metadata, ns) except Exception as err: logger.error("Exception: %s" % err) return "api_error: Unable to parse dependencies.", 400 logger.debug(dependencies) # and finaly, update our database. logger.debug("Updating database entries.") db.insert_or_update_package(session, package_name=pkg_name, title=et_to_str( metadata.find('nuspec:title', ns)), latest_version=version) pkg_id = (session.query( db.Package).filter(db.Package.name == pkg_name).one()).package_id logger.debug("package_id = %d" % pkg_id) db.insert_version( session, authors=et_to_str(metadata.find('nuspec:authors', ns)), copyright_=et_to_str(metadata.find('nuspec:copyright', ns)), dependencies=dependencies, description=et_to_str(metadata.find('nuspec:description', ns)), package_hash=hash_, package_hash_algorithm='SHA512', package_size=filesize, icon_url=et_to_str(metadata.find('nuspec:iconUrl', ns)), is_prerelease='-' in version, license_url=et_to_str(metadata.find('nuspec:licenseUrl', ns)), owners=et_to_str(metadata.find('nuspec:owners', ns)), package_id=pkg_id, project_url=et_to_str(metadata.find('nuspec:projectUrl', ns)), release_notes=et_to_str(metadata.find('nuspec:releaseNotes', ns)), require_license_acceptance=et_to_str( metadata.find('nuspec:requireLicenseAcceptance', ns)) == 'true', tags=et_to_str(metadata.find('nuspec:tags', ns)), title=et_to_str(metadata.find('nuspec:id', ns)), version=version, ) logger.info( "Sucessfully updated database entries for package %s version %s." % (pkg_name, version)) resp = make_response('', 201) return resp