File: abstract.py

package info (click to toggle)
python-b2sdk 2.8.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 3,020 kB
  • sloc: python: 30,902; sh: 13; makefile: 8
file content (147 lines) | stat: -rw-r--r-- 5,181 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
######################################################################
#
# File: b2sdk/_internal/transfer/inbound/downloader/abstract.py
#
# Copyright 2020 Backblaze Inc. All Rights Reserved.
#
# License https://www.backblaze.com/using_b2_code.html
#
######################################################################
from __future__ import annotations

import hashlib
from abc import abstractmethod
from concurrent.futures import ThreadPoolExecutor
from io import IOBase

from requests.models import Response

from b2sdk._internal.encryption.setting import EncryptionSetting
from b2sdk._internal.file_version import DownloadVersion
from b2sdk._internal.session import B2Session
from b2sdk._internal.utils import B2TraceMetaAbstract
from b2sdk._internal.utils.range_ import Range


class EmptyHasher:
    def __init__(self, *args, **kwargs):
        pass

    def update(self, data):
        pass

    def digest(self):
        return b''

    def hexdigest(self):
        return ''

    def copy(self):
        return self


class AbstractDownloader(metaclass=B2TraceMetaAbstract):
    """
    Abstract class for downloaders.

    :var REQUIRES_SEEKING: if True, the downloader requires the ability to seek in the file object.
    :var SUPPORTS_DECODE_CONTENT: if True, the downloader supports decoded HTTP streams.
        In practice, this means that the downloader can handle HTTP responses which already
        have the content decoded per Content-Encoding and, more likely than not, of a different
        length than requested.
    """

    REQUIRES_SEEKING = True
    SUPPORTS_DECODE_CONTENT = True
    DEFAULT_THREAD_POOL_CLASS = staticmethod(ThreadPoolExecutor)
    DEFAULT_ALIGN_FACTOR = 4096

    def __init__(
        self,
        thread_pool: ThreadPoolExecutor | None = None,
        force_chunk_size: int | None = None,
        min_chunk_size: int | None = None,
        max_chunk_size: int | None = None,
        align_factor: int | None = None,
        check_hash: bool = True,
        **kwargs,
    ):
        align_factor = align_factor or self.DEFAULT_ALIGN_FACTOR
        assert force_chunk_size is not None or (
            min_chunk_size is not None
            and max_chunk_size is not None
            and 0 < min_chunk_size <= max_chunk_size
            and max_chunk_size >= align_factor
        )
        self._min_chunk_size = min_chunk_size
        self._max_chunk_size = max_chunk_size
        self._forced_chunk_size = force_chunk_size
        self._align_factor = align_factor
        self._check_hash = check_hash
        self._thread_pool = (
            thread_pool if thread_pool is not None else self.DEFAULT_THREAD_POOL_CLASS()
        )
        super().__init__(**kwargs)

    def _get_hasher(self):
        if self._check_hash:
            return hashlib.sha1()
        return EmptyHasher()

    def _get_chunk_size(self, content_length: int | None):
        if self._forced_chunk_size is not None:
            return self._forced_chunk_size
        ideal = max(content_length // 1000, self._align_factor)
        non_aligned = min(max(ideal, self._min_chunk_size), self._max_chunk_size)
        aligned = non_aligned // self._align_factor * self._align_factor
        return aligned

    @classmethod
    def _get_remote_range(cls, response: Response, download_version: DownloadVersion):
        """
        Get a range from response or original request (as appropriate).

        :param response: requests.Response of initial request
        :param download_version: b2sdk.v2.DownloadVersion
        :return: a range object
        """
        if 'Range' in response.request.headers:
            return Range.from_header(response.request.headers['Range'])
        return download_version.range_

    def is_suitable(self, download_version: DownloadVersion, allow_seeking: bool):
        """
        Analyze download_version (possibly against options passed earlier to constructor
        to find out whether the given download request should be handled by this downloader).
        """
        if self.REQUIRES_SEEKING and not allow_seeking:
            return False
        if (
            not self.SUPPORTS_DECODE_CONTENT
            and download_version.content_encoding
            and download_version.api.api_config.decode_content
        ):
            return False
        return True

    @abstractmethod
    def download(
        self,
        file: IOBase,
        response: Response,
        download_version: DownloadVersion,
        session: B2Session,
        encryption: EncryptionSetting | None = None,
    ) -> tuple[int, str]:
        """
        Download target to a file-like object.

        :param file: file-like object to write to
        :param response: requests.Response of b2_download_url_by_* endpoint with the target object
        :param download_version: DownloadVersion of an object being downloaded
        :param session: B2Session to be used for downloading
        :param encryption: optional Encryption setting
        :return: (bytes_read, actual_sha1)
            please note bytes_read may be different from bytes written to a file object if decode_content=True
        """
        pass