File: download_dest.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 (241 lines) | stat: -rw-r--r-- 7,018 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
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
######################################################################
#
# File: b2sdk/v1/download_dest.py
#
# Copyright 2019 Backblaze Inc. All Rights Reserved.
#
# License https://www.backblaze.com/using_b2_code.html
#
######################################################################
from __future__ import annotations

import io
import os
from abc import abstractmethod
from contextlib import contextmanager

from b2sdk._internal.stream.progress import WritingStreamWithProgress

from b2sdk._internal.utils import B2TraceMetaAbstract, limit_trace_arguments, set_file_mtime


class AbstractDownloadDestination(metaclass=B2TraceMetaAbstract):
    """
    Interface to a destination for a downloaded file.
    """

    @abstractmethod
    @limit_trace_arguments(
        skip=[
            'content_sha1',
        ]
    )
    def make_file_context(
        self,
        file_id,
        file_name,
        content_length,
        content_type,
        content_sha1,
        file_info,
        mod_time_millis,
        range_=None,
    ):
        """
        Return a context manager that yields a binary file-like object to use for
        writing the contents of the file.

        :param str file_id: the B2 file ID from the headers
        :param str file_name: the B2 file name from the headers
        :param str content_length: the content length
        :param str content_type: the content type from the headers
        :param str content_sha1: the content sha1 from the headers (or ``"none"`` for large files)
        :param dict file_info: the user file info from the headers
        :param int mod_time_millis: the desired file modification date in ms since 1970-01-01
        :param None,tuple[int,int] range_: starting and ending offsets of the received file contents.  Usually ``None``,
                       which means that the whole file is downloaded.
        :return: None
        """


class DownloadDestLocalFile(AbstractDownloadDestination):
    """
    Store a downloaded file into a local file and sets its modification time.
    """

    MODE = 'wb+'

    def __init__(self, local_file_path):
        self.local_file_path = local_file_path

    def make_file_context(
        self,
        file_id,
        file_name,
        content_length,
        content_type,
        content_sha1,
        file_info,
        mod_time_millis,
        range_=None,
    ):
        self.file_id = file_id
        self.file_name = file_name
        self.content_length = content_length
        self.content_type = content_type
        self.content_sha1 = content_sha1
        self.file_info = file_info
        self.range_ = range_
        return self.write_to_local_file_context(mod_time_millis)

    @contextmanager
    def write_to_local_file_context(self, mod_time_millis):
        completed = False
        try:
            # Open the file and let the caller write it.
            with open(self.local_file_path, self.MODE) as f:
                yield f

            set_file_mtime(self.local_file_path, mod_time_millis)

            # Set the flag that means to leave the downloaded file on disk.
            completed = True

        finally:
            # This is a best-effort attempt to clean up files that
            # failed to download, so we don't leave partial files
            # sitting on disk.
            if not completed:
                os.unlink(self.local_file_path)


class PreSeekedDownloadDest(DownloadDestLocalFile):
    """
    Store a downloaded file into a local file and sets its modification time.
    Does not truncate the target file, seeks to a given offset just after opening
    a descriptor.
    """

    MODE = 'rb+'

    def __init__(self, local_file_path, seek_target):
        self._seek_target = seek_target
        super().__init__(local_file_path)

    @contextmanager
    def write_to_local_file_context(self, *args, **kwargs):
        with super().write_to_local_file_context(*args, **kwargs) as f:
            f.seek(self._seek_target)
            yield f


class DownloadDestBytes(AbstractDownloadDestination):
    """
    Store a downloaded file into bytes in memory.
    """

    def __init__(self):
        self.bytes_written = None

    def make_file_context(
        self,
        file_id,
        file_name,
        content_length,
        content_type,
        content_sha1,
        file_info,
        mod_time_millis,
        range_=None,
    ):
        self.file_id = file_id
        self.file_name = file_name
        self.content_length = content_length
        self.content_type = content_type
        self.content_sha1 = content_sha1
        self.file_info = file_info
        self.mod_time_millis = mod_time_millis
        self.range_ = range_
        return self.capture_bytes_context()

    @contextmanager
    def capture_bytes_context(self):
        """
        Remember the bytes written in self.bytes_written.
        """
        # Make a place to store the data written
        bytes_io = io.BytesIO()

        # Let the caller write it
        yield bytes_io

        # Capture the result.  The BytesIO object won't let you grab
        # the data after it's closed
        self.bytes_written = bytes_io.getvalue()
        bytes_io.close()

    def get_bytes_written(self):
        if self.bytes_written is None:
            raise Exception('data not written yet')
        return self.bytes_written


class DownloadDestProgressWrapper(AbstractDownloadDestination):
    """
    Wrap a DownloadDestination and report progress to a ProgressListener.
    """

    def __init__(self, download_dest, progress_listener):
        self.download_dest = download_dest
        self.progress_listener = progress_listener

    def make_file_context(
        self,
        file_id,
        file_name,
        content_length,
        content_type,
        content_sha1,
        file_info,
        mod_time_millis,
        range_=None,
    ):
        return self.write_file_and_report_progress_context(
            file_id,
            file_name,
            content_length,
            content_type,
            content_sha1,
            file_info,
            mod_time_millis,
            range_,
        )

    @contextmanager
    def write_file_and_report_progress_context(
        self,
        file_id,
        file_name,
        content_length,
        content_type,
        content_sha1,
        file_info,
        mod_time_millis,
        range_,
    ):
        with self.download_dest.make_file_context(
            file_id,
            file_name,
            content_length,
            content_type,
            content_sha1,
            file_info,
            mod_time_millis,
            range_,
        ) as file_:
            total_bytes = content_length
            if range_ is not None:
                total_bytes = range_[1] - range_[0] + 1
            self.progress_listener.set_total_bytes(total_bytes)
            with self.progress_listener:
                yield WritingStreamWithProgress(file_, self.progress_listener)