conan-io/conan

View on GitHub
conans/client/rest/file_uploader.py

Summary

Maintainability
A
1 hr
Test Coverage
import os
import time

from copy import copy

from conans.client.rest import response_to_str
from conans.errors import AuthenticationException, ConanException, \
    NotFoundException, ForbiddenException, RequestErrorException, InternalErrorException
from conans.util import progress_bar
from conans.util.files import sha1sum


class FileUploader(object):

    def __init__(self, requester, output, verify, config):
        self._output = output
        self._requester = requester
        self._config = config
        self._verify_ssl = verify

    @staticmethod
    def _handle_400_response(response, auth):
        if response.status_code == 400:
            raise RequestErrorException(response_to_str(response))

        if response.status_code == 401:
            raise AuthenticationException(response_to_str(response))

        if response.status_code == 403:
            if auth is None or auth.token is None:
                raise AuthenticationException(response_to_str(response))
            raise ForbiddenException(response_to_str(response))

    def _dedup(self, url, headers, auth):
        """ send the headers to see if it is possible to skip uploading the file, because it
        is already in the server. Artifactory support file deduplication
        """
        dedup_headers = {"X-Checksum-Deploy": "true"}
        if headers:
            dedup_headers.update(headers)
        response = self._requester.put(url, data="", verify=self._verify_ssl, headers=dedup_headers,
                                       auth=auth)
        if response.status_code == 500:
            raise InternalErrorException(response_to_str(response))

        self._handle_400_response(response, auth)

        if response.status_code == 201:  # Artifactory returns 201 if the file is there
            return response

    def upload(self, url, abs_path, auth=None, dedup=False, retry=None, retry_wait=None,
               headers=None, display_name=None):
        retry = retry if retry is not None else self._config.retry
        retry = retry if retry is not None else 1
        retry_wait = retry_wait if retry_wait is not None else self._config.retry_wait
        retry_wait = retry_wait if retry_wait is not None else 5

        # Send always the header with the Sha1
        headers = copy(headers) or {}
        headers["X-Checksum-Sha1"] = sha1sum(abs_path)
        if dedup:
            response = self._dedup(url, headers, auth)
            if response:
                return response

        for counter in range(retry + 1):
            try:
                return self._upload_file(url, abs_path, headers, auth, display_name)
            except (NotFoundException, ForbiddenException, AuthenticationException,
                    RequestErrorException):
                raise
            except ConanException as exc:
                if counter == retry:
                    raise
                else:
                    if self._output:
                        self._output.error(exc)
                        self._output.info("Waiting %d seconds to retry..." % retry_wait)
                    time.sleep(retry_wait)

    def _upload_file(self, url, abs_path,  headers, auth, display_name):
        file_size = os.stat(abs_path).st_size
        file_name = os.path.basename(abs_path)
        description = "Uploading {}".format(file_name)
        post_description = "Uploaded {}".format(
            file_name) if not display_name else "Uploaded {} -> {}".format(file_name, display_name)

        def load_in_chunks(_file):
            """Lazy function (generator) to read a file piece by piece.
            Default chunk size: 1k."""
            while True:
                chunk = _file.read(1024)
                if not chunk:
                    break
                yield chunk

        with open(abs_path, mode='rb') as file_handler:
            progress = progress_bar.Progress(file_size, self._output, description, post_description)
            data = progress.update(load_in_chunks(file_handler))
            iterable_to_file = IterableToFileAdapter(data, file_size)
            try:
                response = self._requester.put(url, data=iterable_to_file, verify=self._verify_ssl,
                                               headers=headers, auth=auth)
                self._handle_400_response(response, auth)
                response.raise_for_status()  # Raise HTTPError for bad http response status
                return response
            except ConanException:
                raise
            except Exception as exc:
                raise ConanException(exc)


class IterableToFileAdapter(object):
    def __init__(self, iterable, total_size):
        self.iterator = iter(iterable)
        self.total_size = total_size

    def read(self, size=-1):  # @UnusedVariable
        return next(self.iterator, b'')

    def __len__(self):
        return self.total_size

    def __iter__(self):
        return self.iterator.__iter__()