Source code for reuse.download

# SPDX-FileCopyrightText: 2019 Free Software Foundation Europe e.V. <https://fsfe.org>
# SPDX-FileCopyrightText: 2023 Nico Rikken <nico.rikken@fsfe.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later

"""Functions for downloading license files from spdx/license-list-data."""

import errno
import logging
import os
import shutil
import sys
import urllib.request
from argparse import ArgumentParser, Namespace
from gettext import gettext as _
from pathlib import Path
from typing import IO, Optional
from urllib.error import URLError
from urllib.parse import urljoin

from ._licenses import ALL_NON_DEPRECATED_MAP
from ._util import (
    _LICENSEREF_PATTERN,
    PathType,
    StrPath,
    find_licenses_directory,
    print_incorrect_spdx_identifier,
)
from .project import Project
from .report import ProjectReport

_LOGGER = logging.getLogger(__name__)

# All raw text files are available as files underneath this path.
_SPDX_REPOSITORY_BASE_URL = (
    "https://raw.githubusercontent.com/spdx/license-list-data/master/text/"
)


[docs]def download_license(spdx_identifier: str) -> str: """Download the license text from the SPDX repository. Args: spdx_identifier: SPDX identifier of the license. Raises: URLError: if the license could not be downloaded. Returns: The license text. """ # This is fairly naive, but I can't see anything wrong with it. url = urljoin(_SPDX_REPOSITORY_BASE_URL, "".join((spdx_identifier, ".txt"))) _LOGGER.debug("downloading license from '%s'", url) # TODO: Cache result? with urllib.request.urlopen(url) as response: if response.getcode() == 200: return response.read().decode("utf-8") raise URLError("Status code was not 200")
def _path_to_license_file(spdx_identifier: str, root: StrPath) -> Path: licenses_path = find_licenses_directory(root=root) return licenses_path / "".join((spdx_identifier, ".txt"))
[docs]def put_license_in_file( spdx_identifier: str, destination: StrPath, source: Optional[StrPath] = None, ) -> None: """Download a license and put it in the destination file. This function exists solely for convenience. Args: spdx_identifier: SPDX License Identifier of the license. destination: Where to put the license. source: Path to file or directory containing the text for LicenseRef licenses. Raises: URLError: if the license could not be downloaded. FileExistsError: if the license file already exists. FileNotFoundError: if the source could not be found in the directory. """ header = "" destination = Path(destination) destination.parent.mkdir(exist_ok=True) if destination.exists(): raise FileExistsError( errno.EEXIST, os.strerror(errno.EEXIST), str(destination) ) # LicenseRef- license; don't download anything. if _LICENSEREF_PATTERN.match(spdx_identifier): if source: source = Path(source) if source.is_dir(): source = source / f"{spdx_identifier}.txt" if not source.exists(): raise FileNotFoundError( errno.ENOENT, os.strerror(errno.ENOENT), str(source) ) shutil.copyfile(source, destination) else: destination.touch() else: text = download_license(spdx_identifier) with destination.open("w", encoding="utf-8") as fp: fp.write(header) fp.write(text)
[docs]def add_arguments(parser: ArgumentParser) -> None: """Add arguments to parser.""" parser.add_argument( "license", action="store", nargs="*", help=_("SPDX License Identifier of license"), ) parser.add_argument( "--all", action="store_true", help=_("download all missing licenses detected in the project"), ) parser.add_argument( "--output", "-o", dest="file", action="store", type=PathType("w") ) parser.add_argument( "--source", action="store", type=PathType("r"), help=_( "source from which to copy custom LicenseRef- licenses, either" " a directory that contains the file or the file itself" ), )
[docs]def run(args: Namespace, project: Project, out: IO[str] = sys.stdout) -> int: """Download license and place it in the LICENSES/ directory.""" def _already_exists(path: StrPath) -> None: out.write( _("Error: {spdx_identifier} already exists.").format( spdx_identifier=path ) ) out.write("\n") def _not_found(path: StrPath) -> None: out.write(_("Error: {path} does not exist.").format(path=path)) def _could_not_download(identifier: str) -> None: out.write(_("Error: Failed to download license.")) out.write(" ") if identifier not in ALL_NON_DEPRECATED_MAP: print_incorrect_spdx_identifier(identifier, out=out) else: out.write(_("Is your internet connection working?")) out.write("\n") def _successfully_downloaded(destination: StrPath) -> None: out.write( _("Successfully downloaded {spdx_identifier}.").format( spdx_identifier=destination ) ) out.write("\n") if args.all: # TODO: This is fairly inefficient, but gets the job done. report = ProjectReport.generate(project) licenses = report.missing_licenses if args.file: _LOGGER.warning( _("--output has no effect when used together with --all") ) args.file = None elif not args.license: args.parser.error(_("the following arguments are required: license")) elif len(args.license) > 1 and args.file: args.parser.error(_("cannot use --output with more than one license")) else: licenses = args.license return_code = 0 for lic in licenses: if args.file: destination = args.file else: destination = _path_to_license_file(lic, project.root) try: put_license_in_file( lic, destination=destination, source=args.source ) except URLError: _could_not_download(lic) return_code = 1 except FileExistsError as err: _already_exists(err.filename) return_code = 1 except FileNotFoundError as err: _not_found(err.filename) return_code = 1 else: _successfully_downloaded(destination) return return_code